比较 C++、Zig 和 C3 的反射能力
Comparing reflection capabilities of C++, Zig and C3

原始链接: https://nyr24.github.io/blog/reflection-comparison/

本文比较了 C++、Zig 和 C3 中的编译时反射。反射允许程序在编译时检查其自身结构,从而在无需运行时开销的情况下实现序列化和调试等任务。 * **C++** 通过复杂且通常冗长的模板机制以及新的元编程特性来实现反射。 * **Zig** 利用 `comptime` 函数、`inline` 循环和内置的类型自省功能。它现代且易读,但缺乏用于某些特定任务(如为结构体成员添加自定义属性)的原生宏系统。 * **C3** 利用一套独特的宏系统,其中编译时构造以 `$` 为前缀。这种显式的语法清晰地将编译时逻辑与运行时代码分离开来。C3 还具备用于输入验证的强大“契约”功能以及对属性的原生支持,这使得其元编程极具表现力且易于阅读。 作者认为,尽管这三种语言都能有效处理反射,但 C3 在清晰度、稳定性和实用设计方面脱颖而出。随着 C3 逐渐接近 1.0 版本,作者建议将其视为现代系统编程中 C++ 和 Zig 的一个极具竞争力且稳定的替代方案,值得行业给予更多关注。

Hacker News 最新 | 往期 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 比较 C++、Zig 和 C3 的反射能力 ( nyr24.github.io ) 7 点 由 ManuLinares 1 小时前 | 隐藏 | 往期 | 收藏 | 2 条评论 帮助 peesem 32 分钟前 | 下一条 [–] “不是 Zig 专家”,但你连查阅文档找到 `@tagName` 内置函数来解决提出的问题都做不到吗?: https://ziglang.org/documentation/0.16.0/#tagName 我随便谷歌一下“zig enum to string”,第一个结果就是 Reddit 帖子,里面给出的解决方案就是 `@tagName`。我准备把这篇文章当作是 C3 的低质量广告了。 回复 WalterBright 30 分钟前 | 上一条 [–] 有一个巨大的黑色“之”字形挡住了文字,没法阅读。 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Reflection lets a program inspect and manipulate its own structure at runtime or compile time. All C++ (with its upcoming reflection support), Zig and C3 rely on compile-time reflection, so you can reason about types, enumerators, and struct members without any runtime cost. In this post I will compare how these languages approach compile-time reflection.

What is C3?

C3 is a relatively new programming language which mainly focuses on readability, performance, minimalism, and familiarity for C/C++ programmers.
It doesn't have heavy runtime, garbage collection, exceptions or RAII.
It also fully supports C ABI compatibility out of the box.

C3 uses special syntax for compile-time execution: all variables, control-flow constructs are prefixed with $. This was done on purpose to explicitly show the reader which code runs at compile time. It uses macros for compile-time evaluation and reflection.

C3 macros are designed to provide a replacement for C preprocessor macros. They extend such macros by providing compile-time evaluation using constant folding, which offers an IDE friendly, limited, compile-time execution.

Let’s see all languages in action!

Enum to string conversion

C++:

enum class Color { Red, Green, Blue };

constexpr std::string_view enum_to_string(E value) {

template inline for (constexpr auto r : std::meta::enumerators_of(^^E)) {

return std::meta::identifier_of(r);

Color color = Color::Red;

printf("%s", enum_to_string(color));

Zig:

pub fn to_string(color: Color) []const u8 {

.GREEN => return "green",

std.debug.print("{s}", .{c.to_string()});

In Zig, the only solution I can think of is attaching a method to each enum you want to turn into a string, not a generic approach. I’m not a profound zig expert so you can correct me in the comments.

C3:

enum Color { RED, GREEN, BLUE }

macro String enum_to_string($enum_val)

var $EnumType = $Typeof($enum_val);

$foreach $val : $EnumType::values:

String $color_name = enum_to_string($color);

io::printfn("%s", $color_name);

In C3 enums have special properties. For example, if you want to print enum value, it will print it in a readable form, exactly as defined in the source code. For example, this code: io::printfn(“%s”, Color.RED) will output RED, not 0.
If you want to take the underlying value from an enum, you can either access .ordinal or cast it to the underlying type.
You can also associate values of any type with your enumerators:

enum Color : uint (String str_repr, char amount_of_red)

fn void log_color(Color c)

io::printfn("%s %s", c.str_repr, c.amount_of_red); // Outputs: Red Color 255

Let’s proceed with reflections!

Struct introspection

C++:

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";

Person alice{"Alice Smith", 30, 1.75};

print_struct_fields(alice);

Zig:

fn printStructFields(value: anytype) void {

std.debug.assert(@typeInfo(@TypeOf(value)) == .@"struct");

inline for (@typeInfo(@TypeOf(value)).@"struct".fields) |field| {

std.debug.print("{s}: {s},\n", .{ field.name, @field(value, field.name) });

std.debug.print("{s}: {any},\n", .{ field.name, @field(value, field.name) });

std.debug.print("Person Details:\n", .{});

printStructFields(alice);

C3:

@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);

Person $alice = {"Alice Smith", 30, 1.75};

io::printfn("Person details: ");

print_struct_fields($alice);

Here, (1) C3 uses optional pre-conditions called 'contracts' which can help drastically with input validation. They will be executed at compile-time if it is possible, if not - at runtime.

Validation with compile-time only attributes

C++:

struct Range { int lo; int hi; }

[[=Range{ 1, 65535 }]] int port;

[[=Range{ 1, 256 }]] int max_threads;

[[=Range{ 100, 30000 }]] int timeout_ms;

consexpr 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;

static_assert(validate(Config{ 1000, 50, 20000 }));

static_assert(validate(Config{ 0, 0, 0 })); // Fails to compile.

Zig:
Zig unfortunately doesn’t have ‘attributes’ or any substitute to attach compile-time data to struct members.

C3:

struct Range { int lo; int hi; }

attrdef @Range(r) = @tag("range", r);

int port @Range({1, 65535});

int max_threads @Range({1, 256});

int timeout_ms @Range({100, 30000});

enum ValidationResult { TO_LOW, TO_HIGH, SUCCESS }

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");

macro ValidationResult validate_runtime(obj)

var $Type = $Typeof(obj);

$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;

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));

For this example with C3 I want to show you 2 options. In the first (1) variant we validate everything at compile-time, we can verify this easily by putting @const attribute on the macro. In the second (2) variant we’re mixing compile-time attributes with validation at runtime. In this example you can see how syntax distinction between $if and if helps to understand which code gets expanded at compile-time and which will execute at runtime.

Conclusions

All observed languages can do real compile-time reflection, which is great for serializers, debug printers, and generic helpers like the ones above.
The tradeoff is ergonomics: C++ gets the power via verbose template machinery and splices, while C3 makes the same ideas more readable and expressive through its macro system and special syntax for compile-time execution, it's very easy to understand where code will execute at compile time and where it wouldn't.

Zig in turn doesn't have macros, instead it relies on comptime functions and blocks, inline for loops and type-introspection builtins, which is also a good, modern and mostly readable approach.

Personally, I've found C3 to be a very promising systems programming language that needs more attention; everybody knows about C++ and Zig is marketed very well, but C3 lacks that kind of marketing, though it can compete easily with Zig, Odin, or any other new systems programming language out there.
Also it doesn't have tons of breaking changes with each minor version. It's a lot more stable than Zig (honestly, it's pretty embarrassing that Zig is still stuck on 0.1x versions after over 10 years of development), and since C3 is already on 0.8.x versions, 1.0 is very close, see
the roadmap.

You can search for more info about C3 on the main website.
Want to discuss the language or have a question? Join official C3 server on Discord.

联系我们 contact @ memedata.com