プログラミング
C++, Zig、およびC3のリフレクション機能の比較
Comparing reflection capabilities of C++, Zig and C3 (nyr24.github.io)
要約
この記事では、C++(将来のサポート)、Zig、C3の3つの言語におけるコンパイル時リフレクションの機能と実装を比較しています。各言語は、実行時コストなしで型、列挙子、構造体メンバーを検査・操作する方法を示し、特に列挙型から文字列への変換と構造体のイントロスペクションの例を挙げています。
全文翻訳
リフレクションにより、プログラムは実行時またはコンパイル時に自身の構造を検査および操作できます。C++(将来のリフレクションサポートを含む)、Zig、C3はすべてコンパイル時リフレクションに依存しているため、実行時コストなしで型、列挙子、構造体メンバーについて推論できます。この記事では、これらの言語がコンパイル時リフレクションにどのようにアプローチするかを比較します。
C3とは何ですか?
C3は、主に可読性、パフォーマンス、ミニマリズム、およびC/C++プログラマーにとっての親しみやすさに焦点を当てた、比較的新しいプログラミング言語です。重いランタイム、ガベージコレクション、例外、RAIIはありません。また、C ABI互換性を最初から完全にサポートしています。
C3はコンパイル時実行のために特別な構文を使用します。すべての変数、制御フロー構造は$でプレフィックスされます。これは、どのコードがコンパイル時に実行されるかを読者に明示的に示すために意図的に行われました。マクロを使用してコンパイル時評価とリフレクションを行います。C3のマクロは、Cプリプロセッサマクロの代替を提供するように設計されています。これらは、定数畳み込みを使用したコンパイル時評価を提供することで、そのようなマクロを拡張し、IDEフレンドリーで限定的なコンパイル時実行を提供します。
すべての言語を実際に見てみましょう!
列挙型から文字列への変換
C++:
1enum class Color { Red, Green, Blue };
2
3template <typename E>
4constexpr std::string_view enum_to_string(E value) {
5 template inline for (constexpr auto r : std::meta::enumerators_of(^^E)) {
6 if (value == [:r:]) {
7 return std::meta::identifier_of(r);
8 }
9 }
10 return "Unknown";
11}
12
13int main() {
14 Color color = Color::Red;
15 printf("%s", enum_to_string(color));
16
17 return 0;
18}
Zig:
1const Color = enum {
2 RED,
3 GREEN,
4 BLUE,
5};
6
7pub fn to_string(color: Color) []const u8 {
8 switch (color) {
9 .RED => return "red",
10 .GREEN => return "green",
11 .BLUE => return "blue",
12 }
13}
14
15pub fn main() !void {
16 const c: Color = .BLUE;
17 std.debug.print("{s}", .{c.to_string()});
18 // 出力:
19 // blue
20}
Zigでは、文字列に変換したい各列挙型にメソッドを添付するのが唯一の解決策であり、汎用的なアプローチではありません。私は熟練したZigエキスパートではないので、コメントで訂正してください。
C3:
1enum Color { RED, GREEN, BLUE }
2
3macro String enum_to_string($enum_val) {
4 var $EnumType = $Typeof($enum_val);
5 $foreach $val : $EnumType::values {
6 $if $val == $enum_val {
7 return $val.description;
8 }
9 }
10}
11
12fn void main() {
13 Color $color = RED;
14 String $color_name = enum_to_string($color);
15 io::printfn("%s", $color_name);
16}
C3では、列挙型には特別なプロパティがあります。たとえば、列挙型の値を印刷したい場合、ソースコードで定義されているとおりに読みやすい形式で印刷されます。たとえば、このコード:
io::printfn(“%s”, Color.RED) は、0ではなくREDを出力します。
列挙型の基になる値を取得したい場合は、.ordinalにアクセスするか、基になる型にキャストします。また、列挙子に任意の型の値を関連付けることもできます。
1enum Color : uint (String str_repr, char amount_of_red) {
2 RED { "Red Color", 255 }
3 BLUE { "Blue Color", 0 }
4}
5
6fn void log_color(Color c) {
7 io::printfn("%s %s", c.str_repr, c.amount_of_red);
8 // 出力: Red Color 255
9}
リフレクションに進みましょう!
構造体イントロスペクション
C++:
1struct Person {
2 std::string_view name;
3 int age;
4 double height;
5};
6
7template <typename T>
8void print_struct_fields(const T& obj) {
9 std::cout << std::meta::identifier_of(^^T) << " details:\n";
10
11 template inline for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T)) {
12 constexpr std::string_view member_name = std::meta::identifier_of(member);
13 std::cout << " " << member_name << ": " << obj.[:member:] << "\n";
14 }
15}
16
17int main() {
18 Person alice{"Alice Smith", 30, 1.75};
19 print_struct_fields(alice);
20 /*
21 出力:
22 Person details:
23 name: Alice Smith
24 age: 30
25 height: 1.75
26 */
27}
Zig:
1const Person = struct {
2 name: []const u8,
3 age: i32,
4 height: f64,
5};
6
7fn printStructFields(value: anytype) void {
8 comptime {
9 std::debug.assert(@typeInfo(@TypeOf(value)) == .{"struct"});
10 }
11 inline for (@typeInfo(@TypeOf(value)).{"struct"}.fields) |field| {
12 switch (field.type) {
13 []const u8 => {
14 std::debug.print("{s}: {s},\n", .{ field.name, @field(value, field.name) });
15 },
16 else => {
17 std::debug.print("{s}: {any},\n", .{ field.name, @field(value, field.name) });
18 },
19 }
20 }
21}
22
23pub fn main() !void {
24 const alice = Person{
25 .name = "Alice Smith",
26 .age = 30,
27 .height = 1.75,
28 };
29
30 std.debug.print("Person Details:\n", .{});
31 printStructFields(alice);
32 // 出力:
33 // Person details:
34 // name: Alice Smith
35 // age: 30
36 // height: 1.750000
37 }
C3:
1struct Person {
2 String name;
3 int age;
4 double height;
5}
6
7<*
8 @require @kindof($val) == STRUCT : "Expected a struct" // (1)
*>
9macro void print_struct_fields($val) {
10 var $Type = $Typeof($val);
11 $foreach $field : $Type::members {
12 io::printfn("\t%s: %s", $field.name, $val.$field);
13 }
14}
15
16fn void main() {
17 Person $alice = {"Alice Smith", 30, 1.75};
18 io::printfn("Person details: ");
19 print_struct_fields($alice);
20 /*
21 出力:
22 Person details:
23 name: Alice Smith
24 age: 30
25 height: 1.750000
26 */
27}
ここで、(1) C3は、入力検証を大幅に改善できるオプションの事前条件である「契約」を使用します。可能な場合はコンパイル時に実行され、そうでない場合は実行時に実行されます。
コンパイル時のみの属性による検証
C++:
1struct Range { int lo; int hi; }
2
3struct Config {
4 [[=Range{ 1, 65535 }]] int port;
5 [[=Range{ 1, 256 }]] int max_threads;
6 [[=Range{ 100, 30000 }]] int timeout_ms;
7}
8
9template<typename T>
10constexpr bool validate(const T& obj) {
11 constexpr auto context = std::meta::access_context::current();
12 template for (constexpr auto member: define_static_array(
13 nonstatic_data_members_of(^^T, context))) {
14 template for (constexpr auto annotation : define_static_array(
15 annotations_of_with_type(member, ^^Range))) {
16 auto [lo, hi] = extract<Range>(annotation);
17 if (obj.[:member:] < lo) return false;
18 else if (obj.[:member:] > hi) return false;
19 }
20 }
21 return true;
22}
23
24static_assert(validate(Config{ 1000, 50, 20000 }));
25static_assert(validate(Config{ 0, 0, 0 })); // コンパイルに失敗します。
Zig:
Zigには残念ながら「属性」や、構造体メンバーにコンパイル時データを添付するための代替手段はありません。
C3:
1struct Range { int lo; int hi; }
2
3attrdef @Range(r) = @tag("range", r);
4
5struct Config {
6 int port @Range({1, 65535});
7 int max_threads @Range({1, 256});
8 int timeout_ms @Range({100, 30000});
9}
10
11enum ValidationResult { TO_LOW, TO_HIGH, SUCCESS }
12
13// (1)
14macro ValidationResult validate_comptime($obj) @const {
15 var $Type = $Typeof($obj);
16
17 $foreach $field : $Type::members {
18 $if $field.has_tag("range") {
19 Range $r = $field.get_tag("range");
20 $if $obj.$field < $r.lo {
21 return TO_LOW;
22 }
23 $endif
24 $if $obj.$field > $r.hi {
25 return TO_HIGH;
26 }
27 $endif
28 }
29 }
30 return SUCCESS;
31}
32
33// (2)
34macro ValidationResult validate_runtime(obj) {
35 var $Type = $Typeof(obj);
36 Range r @noinit;
37
38 $foreach $field : $Type::members {
39 $if $field.has_tag("range") {
40 r = $field.get_tag("range");
41 if (obj.$field < r.lo) return TO_LOW;
42 if (obj.$field > r.hi) return TO_HIGH;
43 }
44 }
45 return SUCCESS;
46}
47
48fn void main() {
49 Config $c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };
50 Config $c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };
51 Config c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };
52 Config c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };
53
54 io::printn(validate_comptime($c1));
55 io::printn(validate_comptime($c2));
56 io::printn(validate_runtime(c1));
57 io::printn(validate_runtime(c2));
58 /*
59 出力:
60 SUCCESS
61 TO_LOW
62 SUCCESS
63 TO_LOW
64 */
65}
この例では、C3で2つのオプションを示します。最初の(1)のバリアントでは、すべてをコンパイル時に検証します。@const属性をマクロに付けることで、これを簡単に確認できます。2番目の(2)のバリアントでは、コンパイル時属性と実行時検証を組み合わせています。この例では、y