{"article":{"slug":"comparing-reflection-capabilities-of-c-zig-and-c3","title":"Comparing reflection capabilities of C++, Zig and C3","subtitle":null,"summary":"A side-by-side look at compile-time reflection in upcoming C++, Zig, and C3—how each language inspects types, enumerators, and struct members without runtime cost.","content_type":"blog_post","language":"en","canonical_url":"https://nyr24.github.io/blog/reflection-comparison/","author":{"name":"nyr24","url":"https://nyr24.github.io/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"nyr24","url":"https://nyr24.github.io/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"C","slug":"c","url":"https://listedarticles.com/topics/c"},{"name":"Programming Languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1568,"reading_minutes":7,"published_at":"2026-09-20T00:00:00.000Z","added_at":"2026-09-20T03:08:28.490Z","updated_at":"2026-09-20T03:08:28.490Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/comparing-reflection-capabilities-of-c-zig-and-c3","markdown_url":"https://listedarticles.com/articles/comparing-reflection-capabilities-of-c-zig-and-c3.md","example":false,"citation":"nyr24, nyr24. \"Comparing reflection capabilities of C++, Zig and C3.\" 20 Sept 2026. https://nyr24.github.io/blog/reflection-comparison/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://nyr24.github.io/blog/reflection-comparison/"},"body_markdown":"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.\n\n### What is C3?\n\n C3 is a relatively new programming language which mainly focuses on readability, performance, minimalism, and familiarity for C/C++ programmers.\n \n\n It doesn't have heavy runtime, garbage collection, exceptions or RAII.\n \n\n It also fully supports C ABI compatibility out of the box.\n\nC3 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.\nIt uses **macros** for compile-time evaluation and reflection.\n\n> \nC3 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.\n\nLet’s see all languages in action!\n\n### Enum to string conversion\n\nC++:\n\n```\n1enum class Color { Red, Green, Blue };2\n3template <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\n13int main()14{15  Color color = Color::Red;16  printf(\"%s\", enum_to_string(color));17\n18  return 0;19}\n```\n\nZig:\n\n```\n1const Color = enum {2    RED,3    GREEN,4    BLUE,5\n6    pub fn to_string(color: Color) []const u8 {7        switch (color) {8            .RED => return \"red\",9            .GREEN => return \"green\",10            .BLUE => return \"blue\",11        }12    }13};14\n15pub fn main() !void {16    const c: Color = .BLUE;17    std.debug.print(\"{s}\", .{c.to_string()});18    // Outputs:19    // blue20}\n```\n\nIn 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.\nI’m not a profound zig expert so you can correct me in the comments.\n\nC3:\n\n```\n1enum Color { RED, GREEN, BLUE }2\n3macro String enum_to_string($enum_val)4{5    var $EnumType = $Typeof($enum_val);6    $foreach $val : $EnumType::values:7        $if $val == $enum_val:8            return $val.description;9        $endif10    $endforeach11}12\n13fn void main()14{15    Color $color = RED;16    String $color_name = enum_to_string($color);17    io::printfn(\"%s\", $color_name);18}\n```\n\nIn 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.\nFor example, this code: `io::printfn(“%s”, Color.RED)` will output RED, not 0.\n\nIf you want to take the underlying value from an enum, you can either access `.ordinal` or cast it to the underlying type.\n\nYou can also associate values of any type with your enumerators:\n\n```\n1enum Color : uint (String str_repr, char amount_of_red)2{3    RED  { \"Red Color\", 255 }4    BLUE { \"Blue Color\", 0 }5}6\n7fn void log_color(Color c)8{9    io::printfn(\"%s %s\", c.str_repr, c.amount_of_red); // Outputs: Red Color 25510}\n```\n\nLet’s proceed with reflections!\n\n### Struct introspection\n\nC++:\n\n```\n1struct Person {2    std::string_view name;3    int age;4    double height;5};6\n7template <typename T>8void print_struct_fields(const T& obj) {9    std::cout << std::meta::identifier_of(^^T) << \" details:\\n\";10\n11    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\n17int main() {18    Person alice{\"Alice Smith\", 30, 1.75};19    print_struct_fields(alice);20    /*21    Outputs:22    Person details:23      name: Alice Smith24      age: 3025      height: 1.7526    */27}\n```\n\nZig:\n\n```\n1const Person = struct {2    name: []const u8,3    age: i32,4    height: f64,5};6\n7fn 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\n23pub fn main() !void {24    const alice = Person{25        .name = \"Alice Smith\",26        .age = 30,27        .height = 1.75,28    };29\n30    std.debug.print(\"Person Details:\\n\", .{});31    printStructFields(alice);32    // Outputs:33    // Person details:34    // name: Alice Smith35    // age: 3036    // height: 1.75000037}\n```\n\nC3:\n\n```\n1struct Person2{3  String name;4  int age;5  double height;6}7\n8<*9 @require @kindof($val) == STRUCT : \"Expected a struct\" // (1)10*>11macro void print_struct_fields($val)12{13    var $Type = $Typeof($val);14    $foreach $field : $Type::members:15        io::printfn(\"\\t%s: %s\", $field.name, $val.$field);16    $endforeach17}18\n19fn void main()20{21    Person $alice = {\"Alice Smith\", 30, 1.75};22    io::printfn(\"Person details: \");23    print_struct_fields($alice);24    /*25      Outputs:26      Person details:27      name: Alice Smith28      age: 3029      height: 1.75000030    */31}\n```\n\nHere, (1) C3 uses optional pre-conditions called 'contracts' which can help drastically with input validation.\nThey will be executed at compile-time if it is possible, if not - at runtime.\n\n### Validation with compile-time only attributes\n\nC++:\n\n```\n1struct Range { int lo; int hi; }2\n3struct Config4{5    [[=Range{ 1, 65535 }]]   int port;6    [[=Range{ 1, 256 }]]     int max_threads;7    [[=Range{ 100, 30000 }]] int timeout_ms;8}9\n10template<typename T>11consexpr bool validate(const T& obj)12{13    constexpr auto context = std::meta::access_context::current();14    template for (constexpr auto member: define_static_array(15        nonstatic_data_members_of(^^T, context)) {16        template for (constexpr auto annotation : define_static_array(17            annotations_of_with_type(member, ^^Range))) {18            auto [lo, hi] = extract<Range>(annotation);19            if (obj.[:member:] < lo) return false;20            else if (obj.[:member:] > hi) return false;21        })22    return true;23}24\n25static_assert(validate(Config{ 1000, 50, 20000 }));26static_assert(validate(Config{ 0, 0, 0 })); // Fails to compile.\n```\n\nZig:\n\nZig unfortunately doesn’t have ‘attributes’ or any substitute to attach compile-time data to struct members.\n\nC3:\n\n```\n1struct Range { int lo; int hi; }2\n3attrdef @Range(r) = @tag(\"range\", r);4\n5struct Config6{7    int port @Range({1, 65535});8    int max_threads @Range({1, 256});9    int timeout_ms @Range({100, 30000});10}11\n12enum ValidationResult { TO_LOW, TO_HIGH, SUCCESS }13\n14// (1)15macro ValidationResult validate_comptime($obj) @const16{17    var $Type = $Typeof($obj);18\n19    $foreach $field : $Type::members:20        $if $field.has_tag(\"range\"):21            Range $r = $field.get_tag(\"range\");22            $if $obj.$field < $r.lo:23                return TO_LOW;24            $endif25            $if $obj.$field > $r.hi:26                return TO_HIGH;27            $endif28        $endif29    $endforeach30    return SUCCESS;31}32\n33// (2)34macro ValidationResult validate_runtime(obj)35{36    var $Type = $Typeof(obj);37    Range r @noinit;38\n39    $foreach $field : $Type::members:40        $if $field.has_tag(\"range\"):41            r = $field.get_tag(\"range\");42            if (obj.$field < r.lo) return TO_LOW;43            if (obj.$field > r.hi) return TO_HIGH;44        $endif45    $endforeach46    return SUCCESS;47}48\n49fn void main()50{51    Config $c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };52    Config $c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };53    Config c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };54    Config c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };55\n56    io::printn(validate_comptime($c1));57    io::printn(validate_comptime($c2));58    io::printn(validate_runtime(c1));59    io::printn(validate_runtime(c2));60    /*61    Outputs:62      SUCCESS63      TO_LOW64      SUCCESS65      TO_LOW66    */67}\n```\n\nFor 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.\nIn 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\nwhich code gets expanded at compile-time and which will execute at runtime.\n\n### Conclusions\n\n All observed languages can do real compile-time reflection, which is great for serializers, debug printers, and generic helpers like the ones above.\nThe tradeoff is ergonomics: C++ gets the power via verbose template machinery and splices,\n while C3 makes the same ideas more readable and expressive through its macro system and special syntax for compile-time execution,\n it's very easy to understand where code will execute at compile time and where it wouldn't.\n\n 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.\n\n 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.\n \n\n 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](https://c3-lang.org/getting-started/roadmap/).\n\nYou can search for more info about C3 on [the main website](https://c3-lang.org).\n\nWant to discuss the language or have a question? Join official [C3 server on Discord.](https://discord.gg/qN76R87)","body_html":"<p>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.</p>\n<h3 id=\"what-is-c3\">What is C3?</h3>\n<p> C3 is a relatively new programming language which mainly focuses on readability, performance, minimalism, and familiarity for C/C++ programmers.</p>\n<p> It doesn&#39;t have heavy runtime, garbage collection, exceptions or RAII.</p>\n<p> It also fully supports C ABI compatibility out of the box.</p>\n<p>C3 uses special syntax for compile-time execution: all variables, control-flow constructs are prefixed with <strong>$**. This was done on purpose to explicitly show the reader which code runs at compile time.\nIt uses **macros</strong> for compile-time evaluation and reflection.</p>\n<blockquote><p>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.</p></blockquote>\n<p>Let’s see all languages in action!</p>\n<h3 id=\"enum-to-string-conversion\">Enum to string conversion</h3>\n<p>C++:</p>\n<pre><code>1enum class Color { Red, Green, Blue };2\n3template &lt;typename E&gt;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 &quot;Unknown&quot;;11}12\n13int main()14{15  Color color = Color::Red;16  printf(&quot;%s&quot;, enum_to_string(color));17\n18  return 0;19}</code></pre>\n<p>Zig:</p>\n<pre><code>1const Color = enum {2    RED,3    GREEN,4    BLUE,5\n6    pub fn to_string(color: Color) []const u8 {7        switch (color) {8            .RED =&gt; return &quot;red&quot;,9            .GREEN =&gt; return &quot;green&quot;,10            .BLUE =&gt; return &quot;blue&quot;,11        }12    }13};14\n15pub fn main() !void {16    const c: Color = .BLUE;17    std.debug.print(&quot;{s}&quot;, .{c.to_string()});18    // Outputs:19    // blue20}</code></pre>\n<p>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.\nI’m not a profound zig expert so you can correct me in the comments.</p>\n<p>C3:</p>\n<pre><code>1enum Color { RED, GREEN, BLUE }2\n3macro String enum_to_string($enum_val)4{5    var $EnumType = $Typeof($enum_val);6    $foreach $val : $EnumType::values:7        $if $val == $enum_val:8            return $val.description;9        $endif10    $endforeach11}12\n13fn void main()14{15    Color $color = RED;16    String $color_name = enum_to_string($color);17    io::printfn(&quot;%s&quot;, $color_name);18}</code></pre>\n<p>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.\nFor example, this code: <code>io::printfn(“%s”, Color.RED)</code> will output RED, not 0.</p>\n<p>If you want to take the underlying value from an enum, you can either access <code>.ordinal</code> or cast it to the underlying type.</p>\n<p>You can also associate values of any type with your enumerators:</p>\n<pre><code>1enum Color : uint (String str_repr, char amount_of_red)2{3    RED  { &quot;Red Color&quot;, 255 }4    BLUE { &quot;Blue Color&quot;, 0 }5}6\n7fn void log_color(Color c)8{9    io::printfn(&quot;%s %s&quot;, c.str_repr, c.amount_of_red); // Outputs: Red Color 25510}</code></pre>\n<p>Let’s proceed with reflections!</p>\n<h3 id=\"struct-introspection\">Struct introspection</h3>\n<p>C++:</p>\n<pre><code>1struct Person {2    std::string_view name;3    int age;4    double height;5};6\n7template &lt;typename T&gt;8void print_struct_fields(const T&amp; obj) {9    std::cout &lt;&lt; std::meta::identifier_of(^^T) &lt;&lt; &quot; details:\\n&quot;;10\n11    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 &lt;&lt; &quot;  &quot; &lt;&lt; member_name &lt;&lt; &quot;: &quot; &lt;&lt; obj.[:member:] &lt;&lt; &quot;\\n&quot;;14    }15}16\n17int main() {18    Person alice{&quot;Alice Smith&quot;, 30, 1.75};19    print_struct_fields(alice);20    /*21    Outputs:22    Person details:23      name: Alice Smith24      age: 3025      height: 1.7526    */27}</code></pre>\n<p>Zig:</p>\n<pre><code>1const Person = struct {2    name: []const u8,3    age: i32,4    height: f64,5};6\n7fn printStructFields(value: anytype) void {8    comptime {9        std.debug.assert(@typeInfo(@TypeOf(value)) == .@&quot;struct&quot;);10    }11    inline for (@typeInfo(@TypeOf(value)).@&quot;struct&quot;.fields) |field| {12        switch (field.type) {13            []const u8 =&gt; {14                std.debug.print(&quot;{s}: {s},\\n&quot;, .{ field.name, @field(value, field.name) });15            },16            else =&gt; {17                std.debug.print(&quot;{s}: {any},\\n&quot;, .{ field.name, @field(value, field.name) });18            },19        }20    }21}22\n23pub fn main() !void {24    const alice = Person{25        .name = &quot;Alice Smith&quot;,26        .age = 30,27        .height = 1.75,28    };29\n30    std.debug.print(&quot;Person Details:\\n&quot;, .{});31    printStructFields(alice);32    // Outputs:33    // Person details:34    // name: Alice Smith35    // age: 3036    // height: 1.75000037}</code></pre>\n<p>C3:</p>\n<pre><code>1struct Person2{3  String name;4  int age;5  double height;6}7\n8&lt;*9 @require @kindof($val) == STRUCT : &quot;Expected a struct&quot; // (1)10*&gt;11macro void print_struct_fields($val)12{13    var $Type = $Typeof($val);14    $foreach $field : $Type::members:15        io::printfn(&quot;\\t%s: %s&quot;, $field.name, $val.$field);16    $endforeach17}18\n19fn void main()20{21    Person $alice = {&quot;Alice Smith&quot;, 30, 1.75};22    io::printfn(&quot;Person details: &quot;);23    print_struct_fields($alice);24    /*25      Outputs:26      Person details:27      name: Alice Smith28      age: 3029      height: 1.75000030    */31}</code></pre>\n<p>Here, (1) C3 uses optional pre-conditions called &#39;contracts&#39; which can help drastically with input validation.\nThey will be executed at compile-time if it is possible, if not - at runtime.</p>\n<h3 id=\"validation-with-compile-time-only-attributes\">Validation with compile-time only attributes</h3>\n<p>C++:</p>\n<pre><code>1struct Range { int lo; int hi; }2\n3struct Config4{5    [[=Range{ 1, 65535 }]]   int port;6    [[=Range{ 1, 256 }]]     int max_threads;7    [[=Range{ 100, 30000 }]] int timeout_ms;8}9\n10template&lt;typename T&gt;11consexpr bool validate(const T&amp; obj)12{13    constexpr auto context = std::meta::access_context::current();14    template for (constexpr auto member: define_static_array(15        nonstatic_data_members_of(^^T, context)) {16        template for (constexpr auto annotation : define_static_array(17            annotations_of_with_type(member, ^^Range))) {18            auto [lo, hi] = extract&lt;Range&gt;(annotation);19            if (obj.[:member:] &lt; lo) return false;20            else if (obj.[:member:] &gt; hi) return false;21        })22    return true;23}24\n25static_assert(validate(Config{ 1000, 50, 20000 }));26static_assert(validate(Config{ 0, 0, 0 })); // Fails to compile.</code></pre>\n<p>Zig:</p>\n<p>Zig unfortunately doesn’t have ‘attributes’ or any substitute to attach compile-time data to struct members.</p>\n<p>C3:</p>\n<pre><code>1struct Range { int lo; int hi; }2\n3attrdef @Range(r) = @tag(&quot;range&quot;, r);4\n5struct Config6{7    int port @Range({1, 65535});8    int max_threads @Range({1, 256});9    int timeout_ms @Range({100, 30000});10}11\n12enum ValidationResult { TO_LOW, TO_HIGH, SUCCESS }13\n14// (1)15macro ValidationResult validate_comptime($obj) @const16{17    var $Type = $Typeof($obj);18\n19    $foreach $field : $Type::members:20        $if $field.has_tag(&quot;range&quot;):21            Range $r = $field.get_tag(&quot;range&quot;);22            $if $obj.$field &lt; $r.lo:23                return TO_LOW;24            $endif25            $if $obj.$field &gt; $r.hi:26                return TO_HIGH;27            $endif28        $endif29    $endforeach30    return SUCCESS;31}32\n33// (2)34macro ValidationResult validate_runtime(obj)35{36    var $Type = $Typeof(obj);37    Range r @noinit;38\n39    $foreach $field : $Type::members:40        $if $field.has_tag(&quot;range&quot;):41            r = $field.get_tag(&quot;range&quot;);42            if (obj.$field &lt; r.lo) return TO_LOW;43            if (obj.$field &gt; r.hi) return TO_HIGH;44        $endif45    $endforeach46    return SUCCESS;47}48\n49fn void main()50{51    Config $c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };52    Config $c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };53    Config c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };54    Config c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };55\n56    io::printn(validate_comptime($c1));57    io::printn(validate_comptime($c2));58    io::printn(validate_runtime(c1));59    io::printn(validate_runtime(c2));60    /*61    Outputs:62      SUCCESS63      TO_LOW64      SUCCESS65      TO_LOW66    */67}</code></pre>\n<p>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 <code>@const</code> attribute on the macro.\nIn the second (2) variant we’re mixing compile-time attributes with validation at runtime. In this example you can see how syntax distinction between <code>$if</code> and <code>if</code> helps to understand\nwhich code gets expanded at compile-time and which will execute at runtime.</p>\n<h3 id=\"conclusions\">Conclusions</h3>\n<p> All observed languages can do real compile-time reflection, which is great for serializers, debug printers, and generic helpers like the ones above.\nThe tradeoff is ergonomics: C++ gets the power via verbose template machinery and splices,\n while C3 makes the same ideas more readable and expressive through its macro system and special syntax for compile-time execution,\n it&#39;s very easy to understand where code will execute at compile time and where it wouldn&#39;t.</p>\n<p> Zig in turn doesn&#39;t have macros, instead it relies on <code>comptime</code> functions and blocks, inline for loops and type-introspection builtins, which is also a good, modern and mostly readable approach.</p>\n<p> Personally, I&#39;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.</p>\n<p> Also it doesn&#39;t have tons of breaking changes with each minor version. It&#39;s a lot more stable than Zig (honestly, it&#39;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 <a href=\"https://c3-lang.org/getting-started/roadmap/\" rel=\"nofollow ugc noopener\">the roadmap</a>.</p>\n<p>You can search for more info about C3 on <a href=\"https://c3-lang.org\" rel=\"nofollow ugc noopener\">the main website</a>.</p>\n<p>Want to discuss the language or have a question? Join official <a href=\"https://discord.gg/qN76R87\" rel=\"nofollow ugc noopener\">C3 server on Discord.</a></p>","headings":[{"level":3,"text":"What is C3?","id":"what-is-c3"},{"level":3,"text":"Enum to string conversion","id":"enum-to-string-conversion"},{"level":3,"text":"Struct introspection","id":"struct-introspection"},{"level":3,"text":"Validation with compile-time only attributes","id":"validation-with-compile-time-only-attributes"},{"level":3,"text":"Conclusions","id":"conclusions"}]}}