C++, Zig、およびC3のリフレクション機能の比較
Comparing reflection capabilities of C++, Zig and C3 (nyr24.github.io)
要約
この記事では、C++(将来のサポート)、Zig、C3の3つの言語におけるコンパイル時リフレクションの機能と実装を比較しています。各言語は、実行時コストなしで型、列挙子、構造体メンバーを検査・操作する方法を示し、特に列挙型から文字列への変換と構造体のイントロスペクションの例を挙げています。
全文翻訳
C++、Zig、C3の反射機能の比較
リフレクション(Reflection)とは、プログラムが実行時またはコンパイル時に自身の構造を検査・操作できる機能です。C++(今後のリフレクションサポートを含む)、Zig、C3はいずれもコンパイル時リフレクションに依存しており、実行コストなしに型、列挙子、構造体メンバーについて推論できます。この記事では、これらの言語がコンパイル時リフレクションにどのようにアプローチするかを比較します。
C3とは?
C3は、可読性、パフォーマンス、ミニマリズム、そしてC/C++プログラマーにとっての親しみやすさに主に焦点を当てた、比較的新しいプログラミング言語です。
重いランタイム、ガベージコレクション、例外、RAIIはありません。
また、C ABI互換性も最初から完全にサポートしています。
C3はコンパイル時実行のために特別な構文を使用します。すべての変数、制御フロー構造にはプレフィックスとして$が付いています。これは、どのコードがコンパイル時に実行されるかを読者に明確に示すために意図的に行われました。コンパイル時評価とリフレクションにはマクロを使用します。
C3のマクロは、Cプリプロセッサマクロの代替を提供するように設計されています。これらは、定数畳み込みを使用したコンパイル時評価を提供することで、IDEフレンドリーで限定的なコンパイル時実行を可能にし、そのようなマクロを拡張します。
それでは、各言語の動作を見てみましょう!
列挙型から文字列への変換
C++:
enum class Color { Red, Green, Blue };template <typename E>constexpr std::string_view enum_to_string(E value) { template inline for (constexpr auto r : std::meta::enumerators_of(^^E)) { if (value == [:r:]) { return std::meta::identifier_of(r); } } return "Unknown";}int main(){ Color color = Color::Red; printf("%s", enum_to_string(color)); return 0;}
Zig:
const Color = enum {
RED,
GREEN,
BLUE,
pub fn to_string(color: Color) []const u8 {
switch (color) {
.RED => return "red",
.GREEN => return "green",
.BLUE => return "blue",
}
}
};
pub fn main() !void {
const c: Color = .BLUE;
std.debug.print("{s}", .{c.to_string()});
// 出力:
// blue
}
Zigでは、列挙型を文字列に変換する唯一の方法は、文字列化したい各列挙型にメソッドをアタッチすることであり、汎用的なアプローチではありません。私はZigの専門家ではないので、コメントで訂正していただけると幸いです。
C3:
enum Color { RED, GREEN, BLUE }
macro String enum_to_string($enum_val){
var $EnumType = $Typeof($enum_val);
$foreach $val : $EnumType::values:
$if $val == $enum_val:
return $val.description;
$endif
$endforeach
}
fn void main(){
Color $color = RED;
String $color_name = enum_to_string($color);
io::printfn("%s", $color_name);
}
C3では、列挙型は特別なプロパティを持っています。例えば、列挙値を出力したい場合、ソースコードで定義されている通り、読みやすい形式で出力されます。例えば、このコード io::printfn(“%s”, Color.RED) は、0ではなくREDと出力します。
基になる値を取得したい場合は、.ordinalにアクセスするか、基になる型にキャストすることができます。
また、列挙子に任意の型の値を関連付けることもできます。
enum Color : uint (String str_repr, char amount_of_red){
RED { "Red Color", 255 }
BLUE { "Blue Color", 0 }
}
fn void log_color(Color c){
io::printfn("%s %s", c.str_repr, c.amount_of_red); // 出力: Red Color 255
}
リフレクションに進みましょう!
構造体のイントロスペクション
C++:
struct Person {
std::string_view name;
int age;
double height;
};
template <typename T>
void print_struct_fields(const T& obj) {
std::cout << std::meta::identifier_of(^^T) << " details:\n";
template inline for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T)) {
constexpr std::string_view member_name = std::meta::identifier_of(member);
std::cout << " " << member_name << ": " << obj.[:member:] << "\n";
}
}
int main() {
Person alice{"Alice Smith", 30, 1.75};
print_struct_fields(alice);
/*
出力:
Person details:
name: Alice Smith
age: 30
height: 1.75
*/
}
Zig:
const Person = struct {
name: []const u8,
age: i32,
height: f64,
};
fn printStructFields(value: anytype) void {
comptime {
std.debug.assert(@typeInfo(@TypeOf(value)) == .@"struct");
}
inline for (@typeInfo(@TypeOf(value)).@"struct".fields) |field| {
switch (field.type) {
[]const u8 => {
std.debug.print("{s}: {s},\n", .{ field.name, @field(value, field.name) });
},
else => {
std.debug.print("{s}: {any},\n", .{ field.name, @field(value, field.name) });
},
}
}
}
pub fn main() !void {
const alice = Person{
.name = "Alice Smith",
.age = 30,
.height = 1.75,
};
std.debug.print("Person Details:\n", .{});
printStructFields(alice);
// 出力:
// Person details:
// name: Alice Smith
// age: 30
// height: 1.750000
}
C3:
struct Person{
String name;
int age;
double height;
}
<* @require @kindof($val) == STRUCT : "Expected a struct" // (1)*>
macro void print_struct_fields($val){
var $Type = $Typeof($val);
$foreach $field : $Type::members:
io::printfn("\t%s: %s", $field.name, $val.$field);
$endforeach
}
fn void main(){
Person $alice = {"Alice Smith", 30, 1.75};
io::printfn("Person details: ");
print_struct_fields($alice);
/*
出力:
Person details:
name: Alice Smith
age: 30
height: 1.750000
*/
}
ここで、(1) C3はオプションの事前条件である「契約(contracts)」を使用しており、入力検証に大きく役立ちます。可能な場合はコンパイル時に実行され、そうでない場合は実行時に実行されます。
コンパイル時のみの属性による検証
C++:
struct Range { int lo; int hi; }
struct Config{
[[=Range{ 1, 65535 }]] int port;
[[=Range{ 1, 256 }]] int max_threads;
[[=Range{ 100, 30000 }]] int timeout_ms;
}
template<typename T>
constexpr bool validate(const T& obj){
constexpr auto context = std::meta::access_context::current();
template for (constexpr auto member: define_static_array(
nonstatic_data_members_of(^^T, context)) {
template for (constexpr auto annotation : define_static_array(
annotations_of_with_type(member, ^^Range))) {
auto [lo, hi] = extract<Range>(annotation);
if (obj.[:member:] < lo) return false;
else if (obj.[:member:] > hi) return false;
}
}
return true;
}
static_assert(validate(Config{ 1000, 50, 20000 }));
static_assert(validate(Config{ 0, 0, 0 })); // コンパイルに失敗します。
Zig:
Zigには残念ながら「属性(attributes)」や、構造体メンバーにコンパイル時データをアタッチするための代替手段はありません。
C3:
struct Range { int lo; int hi; }
attrdef @Range(r) = @tag("range", r);
struct Config{
int port @Range({1, 65535});
int max_threads @Range({1, 256});
int timeout_ms @Range({100, 30000});
}
enum ValidationResult { TO_LOW, TO_HIGH, SUCCESS }
// (1)
macro ValidationResult validate_comptime($obj) @const{
var $Type = $Typeof($obj);
$foreach $field : $Type::members:
$if $field.has_tag("range"):
Range $r = $field.get_tag("range");
$if $obj.$field < $r.lo:
return TO_LOW;
$endif
$if $obj.$field > $r.hi:
return TO_HIGH;
$endif
$endif
$endforeach
return SUCCESS;
}
// (2)
macro ValidationResult validate_runtime(obj){
var $Type = $Typeof(obj);
Range r @noinit;
$foreach $field : $Type::members:
$if $field.has_tag("range"):
r = $field.get_tag("range");
if (obj.$field < r.lo) return TO_LOW;
if (obj.$field > r.hi) return TO_HIGH;
$endif
$endforeach
return SUCCESS;
}
fn void main(){
Config $c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };
Config $c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };
Config c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };
Config c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };
io::printn(validate_comptime($c1));
io::printn(validate_comptime($c2));
io::printn(validate_runtime(c1));
io::printn(validate_runtime(c2));
/*
出力:
SUCCESS
TO_LOW
SUCCESS
TO_LOW
*/
}
C3のこの例では、2つのオプションを示します。最初の(1)のバリアントでは、すべてをコンパイル時に検証します。これは、マクロに@const属性を付けることで簡単に確認できます。2番目の(2)のバリアントでは、コンパイル時属性と実行時検証を組み合わせています。この例では、$ifとifの構文の違いが、どのコードがコンパイル時に展開され、どのコードが実行時に実行されるかを理解するのに役立つことがわかります。
結論
観察されたすべての言語は、シリアライザー、デバッグプリンター、および上記の汎用ヘルパーにとって素晴らしい、真のコンパイル時リフレクションを実行できます。
トレードオフはエルゴノミクスです。C++は冗長なテンプレート機構とスプライスを通じてそのパワーを得ますが、C3はマクロシステムとコンパイル時実行のための特別な構文を通じて、同じアイデアをより読みやすく表現力豊かにします。コードがコンパイル時にどこで実行され、どこで実行されないかを理解するのは非常に簡単です。
Zigは代わりに、comptime関数とブロック、インラインforループ、型イントロスペクションビルトインに依存しており、これもまた、良好でモダンで、ほとんど読みやすいアプローチです。
個人的には、C3はより多くの注目を必要とする非常に有望なシステムプログラミング言語だと感じています。誰もがC++を知っており、Zigは非常によくマーケティングされていますが、C3はそのようなマーケティングが不足しています。しかし、Zig、Odin、または他の新しいシステムプログラミング言語と容易に競争できます。
また、マイナーバージョンごとに大量の破壊的変更があるわけではありません。Zigよりもはるかに安定しています(率直に言って、Zigが10年以上の開発を経てまだ0.1xバージョンに留まっているのはかなり恥ずかしいことです)。そして、C3はすでに0.8.xバージョンなので、1.0は非常に近いです。ロードマップを参照してください。
C3に関する詳細情報はメインウェブサイトで検索できます。
言語について議論したい、または質問がありますか?公式のC3 Discordサーバーに参加してください。