{"article":{"slug":"my-c23-conventions","title":"My C23 Conventions","subtitle":null,"summary":"Abner Coimbre describes how he writes modern C in late 2026 after C23: naming, headers, types, error handling, and the conventions he kept from older 'How I Write C' guidance while adopting new language features.","content_type":"tutorial","language":"en","canonical_url":"https://abner.page/post/c23-conventions-1/","author":{"name":"Abner Coimbre","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Abner","url":"https://abner.page/","listing_slug":null,"listing":null},"topics":[{"name":"C","slug":"c","url":"https://listedarticles.com/topics/c"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"},{"name":"Programming Languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":939,"reading_minutes":4,"published_at":"2026-10-15T00:00:00.000Z","added_at":"2026-10-15T17:20:08.693Z","updated_at":"2026-10-15T17:20:08.693Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/my-c23-conventions","markdown_url":"https://listedarticles.com/articles/my-c23-conventions.md","example":false,"citation":"Abner Coimbre, Abner. \"My C23 Conventions.\" 15 Oct 2026. https://abner.page/post/c23-conventions-1/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://abner.page/post/c23-conventions-1/"},"body_markdown":"# My C23 Conventions\n\nThis is one of a three-part mini series about how I’m using modern C as of late 2026.\n\nBefore C23, I was [taken in](https://www.youtube.com/watch?v=443UNeGrFoM) by *How I Program C* for years and years, and would even feel guilty for choosing C99 instead of `-ansi -pedantic`. But holy crap man I’m done being a doomsday prepper! These days I maintain a large-enough project with enough users where I could stand less foot-shooting. (I’ve also got bills to pay.)\n\nSo here’s three modern C conventions which started to rise naturally from adding `-std=c23` to my zig build:\n\n## 1. Unused? Maybe? (Yes)\n\nOur first one is centered around `[[nodiscard]]`. If I draw an app window or panel I require the caller to interrogate the rect of the thing that was just rendered:\n\n```\n[[nodiscard]] rect_t draw_screen(/* ... */) {\n    rect_t screen_r = { };\n    // Draw stuff\n    // ...\n    return screen_r;\n}\n```\n\nIt’s a business logic bug if my caller doesn’t capture the rectangle for later processing. Therefore `[[nodiscard]]` has been such a godsend for fixing bugs… Except there’s a choice few instances where I will draw something with *no genuine use* for the resulting rect:\n\n```\nenum screen_opt popup_options = /* ... */ ;\ndraw_screen(popup_options, /* ... */); // ERROR: damn you [[nodiscard]]\n```\n\nClang will error out here. I could capture the var and never use it but…\n\n```\nrect_t rect = draw_screen(/* ... */); // ERROR: unused variable (I have `-Werror` enabled)\n```\n\nPre-C23 I could solve this through an `UNUSED()` macro:\n\n```\n#ifndef UNUSED\n#define UNUSED(x) (void)(x)\n#endif\n\nrect_t rect = draw_screen(/* ... */);\nUNUSED(rect); // Silences the compiler, but now I'm sad.\n```\n\nBut now with C23 I’ve deleted the macro in favor of `[[maybe_unused]]`:\n\n```\n[[maybe_unused]] rect_t rect = draw_screen(...); // OK\n```\n\nPhew I feel better. I’ve also noticed this is happening all over the place: lots of different routines returning different types that are marked with `[[nodiscard]]`. So for those few exceptions why not just go for:\n\n```\n[[maybe_unused]] auto _ = foo(); // A new pattern emerges\n```\n\nBonus: For any macro fans out there a fellow indie dev (Zardium) offered an additional imrpovement. From our chat server:\n\n> Zardium: #define \\_ [[maybe\\_unused]] auto CONCAT\\_MACRO(zz,**COUNTER**)\n\n> Abner: ahh, unique varname. nice one\n\n> Zardium: you could take the auto out if you don’t want it to look so weird. then it acts exactly like the \\_ in other languages, where it can be redefined\n\n> Abner: Yep totally. Although here I was celebrating my removal of a macro… XD\n\n## 2. More Perfect Unions\n\nI’ve been cheated and lied to: designated initializers DO NOT clear my struct to zero and I can’t even trust `{0}` anymore. (Talk about a betrayal.) The problem is I’ll often have data types like this contrived timestamp:\n\n```\ntypedef struct {\n    Type type;\n    char buf[MAX];\n    union {\n        time_t time;\n        float dt;\n        date_t date;\n    };\n} stamp_t;\n```\n\nAnd if I partially initialize one:\n\n```\nstamp_t stamp = { .type = STAMP_TYPE_BLA_BLA };\n```\n\nWhile it does mean we successfully zeroed out `buf` (thanks to [empty initialization](https://en.cppreference.com/c/language/initialization#Empty_initialization)) those union members are just a different beast. My dear reader: I’ve made the costly mistake of assuming `date` would clear to zero!\n\nAs I Spongebob-panicked in public Lobsters member [accelbread](https://lobste.rs/~accelbread) offered a sage revelation:\n\n> accelbread: It used to be that GCC would fully zero the union anyways, and clang would only zero the time\\_t, though around a year ago they swapped behaviors iirc. Either way, standard only requires the time\\_t be zeroed and not whole union. GCC has a flag to always zero whole union. Also C23 initialization always inits whole union\n\nWait WHAT? clang and GCC SWAPPED what now? I won’t investigate further. Everything about this is *dangerous af* unless we hug C23 tightly and never let go:\n\n```\nstamp_t stamp = { };\nstamp.type = STAMP_TYPE_BLA_BLA;\n```\n\nAccelbread went on to say two more things:\n\n1. *“Note the C23 zero init whole thing is nothing in the brackets, not 0 in brackets; { 0 } does same thing as before C23”*\n2. *“Also if you control union layout, putting largest member first is safest”*\n\nYessir.\n\n## 3. Enums In C Were Magic (And I Don’t Like It)\n\nIf I want to customize some dialog box I’ll use bit flags:\n\n```\nenum {\n    OPT_AUTOSCROLL = (1 << 0),\n    OPT_NOCURSOR   = (1 << 1),\n    OPT_CENTERED   = (1 << 2),\n    OPT_NOCLOSE    = (1 << 3),\n    // ...\n};\n\n[[nodiscard]] bool draw_warning(const char* msg, int opt);\n```\n\nEven if we disagree with this approach lots of relevant libraries or projects do it:\n\n```\n_ = draw_warning(\"you done messed up A-A-RON\", OPT_CENTERED | OPT_NOCLOSE);\n```\n\nOf course, erroneous bit shifting will land anyone into trouble and yield comical numbers. Thankfully C enums are a plain `int` underneath so the compiler will warn us if we defined a bad constant inside the enum that won’t fit.\n\n:)\n\nI’m kidding. Sigh. Well, I’m growing fearful of sage revelations but this one comes from [Modern C](https://gustedt.gitlabpages.inria.fr/modern-c/) by Jens Gustedt:\n\n> Jens: If enumeration constants do not fit into signed int, if possible, the enumeration type is adjusted such that it can store all enumeration constants.\n\nNoooooo!\n\n> Jens: The property that an an enumeration type is adjusted such that it fits all its constants could have surprising effects\n\n*sobs*\n\n> Jens: So, it is preferable to specify the underlying integer type explicitly\n\nTHERE IS HOPE?\n\n```\nenum options : int {\n    OPT_AUTOSCROLL = (1 << 0),\n    OPT_NOCURSOR   = (1 << 1),\n    OPT_CENTERED   = (1 << 2),\n    OPT_NOCLOSE    = (1 << 3),\n    // ...\n};\n```\n\nIt compiles! We shoot. We score. We stick with C23.\n\n## Epilogue\n\nIn part two of our mini series I’ll discuss three more conventions that I’ve fleshed out thanks to modern C: starting with safer macro usage (forgive me Zig folks.)\n\n*P.S. If you want to shape this discussion [maybe join](https://handmadecities.com/chat) the Handmade Cities server!*\n\n-Abner\n","body_html":"<h1 id=\"my-c23-conventions\">My C23 Conventions</h1>\n<p>This is one of a three-part mini series about how I’m using modern C as of late 2026.</p>\n<p>Before C23, I was <a href=\"https://www.youtube.com/watch?v=443UNeGrFoM\" rel=\"nofollow ugc noopener\">taken in</a> by <em>How I Program C</em> for years and years, and would even feel guilty for choosing C99 instead of <code>-ansi -pedantic</code>. But holy crap man I’m done being a doomsday prepper! These days I maintain a large-enough project with enough users where I could stand less foot-shooting. (I’ve also got bills to pay.)</p>\n<p>So here’s three modern C conventions which started to rise naturally from adding <code>-std=c23</code> to my zig build:</p>\n<h2 id=\"1-unused-maybe-yes\">1. Unused? Maybe? (Yes)</h2>\n<p>Our first one is centered around <code>[[nodiscard]]</code>. If I draw an app window or panel I require the caller to interrogate the rect of the thing that was just rendered:</p>\n<pre><code>[[nodiscard]] rect_t draw_screen(/* ... */) {\n    rect_t screen_r = { };\n    // Draw stuff\n    // ...\n    return screen_r;\n}</code></pre>\n<p>It’s a business logic bug if my caller doesn’t capture the rectangle for later processing. Therefore <code>[[nodiscard]]</code> has been such a godsend for fixing bugs… Except there’s a choice few instances where I will draw something with <em>no genuine use</em> for the resulting rect:</p>\n<pre><code>enum screen_opt popup_options = /* ... */ ;\ndraw_screen(popup_options, /* ... */); // ERROR: damn you [[nodiscard]]</code></pre>\n<p>Clang will error out here. I could capture the var and never use it but…</p>\n<pre><code>rect_t rect = draw_screen(/* ... */); // ERROR: unused variable (I have `-Werror` enabled)</code></pre>\n<p>Pre-C23 I could solve this through an <code>UNUSED()</code> macro:</p>\n<pre><code>#ifndef UNUSED\n#define UNUSED(x) (void)(x)\n#endif\n\nrect_t rect = draw_screen(/* ... */);\nUNUSED(rect); // Silences the compiler, but now I&#39;m sad.</code></pre>\n<p>But now with C23 I’ve deleted the macro in favor of <code>[[maybe_unused]]</code>:</p>\n<pre><code>[[maybe_unused]] rect_t rect = draw_screen(...); // OK</code></pre>\n<p>Phew I feel better. I’ve also noticed this is happening all over the place: lots of different routines returning different types that are marked with <code>[[nodiscard]]</code>. So for those few exceptions why not just go for:</p>\n<pre><code>[[maybe_unused]] auto _ = foo(); // A new pattern emerges</code></pre>\n<p>Bonus: For any macro fans out there a fellow indie dev (Zardium) offered an additional imrpovement. From our chat server:</p>\n<blockquote><p>Zardium: #define _ [[maybe_unused]] auto CONCAT_MACRO(zz,<strong>COUNTER</strong>)</p></blockquote>\n<blockquote><p>Abner: ahh, unique varname. nice one</p></blockquote>\n<blockquote><p>Zardium: you could take the auto out if you don’t want it to look so weird. then it acts exactly like the _ in other languages, where it can be redefined</p></blockquote>\n<blockquote><p>Abner: Yep totally. Although here I was celebrating my removal of a macro… XD</p></blockquote>\n<h2 id=\"2-more-perfect-unions\">2. More Perfect Unions</h2>\n<p>I’ve been cheated and lied to: designated initializers DO NOT clear my struct to zero and I can’t even trust <code>{0}</code> anymore. (Talk about a betrayal.) The problem is I’ll often have data types like this contrived timestamp:</p>\n<pre><code>typedef struct {\n    Type type;\n    char buf[MAX];\n    union {\n        time_t time;\n        float dt;\n        date_t date;\n    };\n} stamp_t;</code></pre>\n<p>And if I partially initialize one:</p>\n<pre><code>stamp_t stamp = { .type = STAMP_TYPE_BLA_BLA };</code></pre>\n<p>While it does mean we successfully zeroed out <code>buf</code> (thanks to <a href=\"https://en.cppreference.com/c/language/initialization#Empty_initialization\" rel=\"nofollow ugc noopener\">empty initialization</a>) those union members are just a different beast. My dear reader: I’ve made the costly mistake of assuming <code>date</code> would clear to zero!</p>\n<p>As I Spongebob-panicked in public Lobsters member <a href=\"https://lobste.rs/~accelbread\" rel=\"nofollow ugc noopener\">accelbread</a> offered a sage revelation:</p>\n<blockquote><p>accelbread: It used to be that GCC would fully zero the union anyways, and clang would only zero the time_t, though around a year ago they swapped behaviors iirc. Either way, standard only requires the time_t be zeroed and not whole union. GCC has a flag to always zero whole union. Also C23 initialization always inits whole union</p></blockquote>\n<p>Wait WHAT? clang and GCC SWAPPED what now? I won’t investigate further. Everything about this is <em>dangerous af</em> unless we hug C23 tightly and never let go:</p>\n<pre><code>stamp_t stamp = { };\nstamp.type = STAMP_TYPE_BLA_BLA;</code></pre>\n<p>Accelbread went on to say two more things:</p>\n<ol><li><em>“Note the C23 zero init whole thing is nothing in the brackets, not 0 in brackets; { 0 } does same thing as before C23”</em></li><li><em>“Also if you control union layout, putting largest member first is safest”</em></li></ol>\n<p>Yessir.</p>\n<h2 id=\"3-enums-in-c-were-magic-and-i-don-t-like-it\">3. Enums In C Were Magic (And I Don’t Like It)</h2>\n<p>If I want to customize some dialog box I’ll use bit flags:</p>\n<pre><code>enum {\n    OPT_AUTOSCROLL = (1 &lt;&lt; 0),\n    OPT_NOCURSOR   = (1 &lt;&lt; 1),\n    OPT_CENTERED   = (1 &lt;&lt; 2),\n    OPT_NOCLOSE    = (1 &lt;&lt; 3),\n    // ...\n};\n\n[[nodiscard]] bool draw_warning(const char* msg, int opt);</code></pre>\n<p>Even if we disagree with this approach lots of relevant libraries or projects do it:</p>\n<pre><code>_ = draw_warning(&quot;you done messed up A-A-RON&quot;, OPT_CENTERED | OPT_NOCLOSE);</code></pre>\n<p>Of course, erroneous bit shifting will land anyone into trouble and yield comical numbers. Thankfully C enums are a plain <code>int</code> underneath so the compiler will warn us if we defined a bad constant inside the enum that won’t fit.</p>\n<p>:)</p>\n<p>I’m kidding. Sigh. Well, I’m growing fearful of sage revelations but this one comes from <a href=\"https://gustedt.gitlabpages.inria.fr/modern-c/\" rel=\"nofollow ugc noopener\">Modern C</a> by Jens Gustedt:</p>\n<blockquote><p>Jens: If enumeration constants do not fit into signed int, if possible, the enumeration type is adjusted such that it can store all enumeration constants.</p></blockquote>\n<p>Noooooo!</p>\n<blockquote><p>Jens: The property that an an enumeration type is adjusted such that it fits all its constants could have surprising effects</p></blockquote>\n<p><em>sobs</em></p>\n<blockquote><p>Jens: So, it is preferable to specify the underlying integer type explicitly</p></blockquote>\n<p>THERE IS HOPE?</p>\n<pre><code>enum options : int {\n    OPT_AUTOSCROLL = (1 &lt;&lt; 0),\n    OPT_NOCURSOR   = (1 &lt;&lt; 1),\n    OPT_CENTERED   = (1 &lt;&lt; 2),\n    OPT_NOCLOSE    = (1 &lt;&lt; 3),\n    // ...\n};</code></pre>\n<p>It compiles! We shoot. We score. We stick with C23.</p>\n<h2 id=\"epilogue\">Epilogue</h2>\n<p>In part two of our mini series I’ll discuss three more conventions that I’ve fleshed out thanks to modern C: starting with safer macro usage (forgive me Zig folks.)</p>\n<p><em>P.S. If you want to shape this discussion <a href=\"https://handmadecities.com/chat\" rel=\"nofollow ugc noopener\">maybe join</a> the Handmade Cities server!</em></p>\n<p>-Abner</p>","headings":[{"level":1,"text":"My C23 Conventions","id":"my-c23-conventions"},{"level":2,"text":"1. Unused? Maybe? (Yes)","id":"1-unused-maybe-yes"},{"level":2,"text":"2. More Perfect Unions","id":"2-more-perfect-unions"},{"level":2,"text":"3. Enums In C Were Magic (And I Don’t Like It)","id":"3-enums-in-c-were-magic-and-i-don-t-like-it"},{"level":2,"text":"Epilogue","id":"epilogue"}]}}