My C23 Conventions
My C23 Conventions Oct 15, 2026 · Abner Coimbre This is one of a three-part mini series about how I’m using modern C as of late 2026. Before C23, I was taken in 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.) So here’s three modern C conventions which started to rise naturally from adding -std=c23 to my zig build: 1. Unused? Maybe? (Yes) Our 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: [[nodiscard]] rect_t draw_screen( /* ... / ) { rect_t screen_r = { }; // Draw stuff // ... return screen_r; } It’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: enum screen_opt popup_options = / ... / ; draw_screen(popup_options, / ... / ); // ERROR: damn you [[nodiscard]] Clang will error out here. I could capture the var and never use it but… rect_t rect = draw_screen( / ... / ); // ERROR: unused variable (I have -Werror enabled) Pre-C23 I could solve this through an UNUSED() macro: #ifndef UNUSED #define UNUSED(x) (void)(x) #endif rect_t rect = draw_screen( / ... */ ); UNUSED(rect); // Silences the compiler, but now I'm sad. But now with C23 I’ve deleted the macro in favor of [[maybe_unused]] : [[maybe_unused]] rect_t rect = draw_screen(...); // OK 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 [[nodiscard]] . So for those few exceptions why not just go for: [[maybe_unused]] auto _ = foo(); // A new pattern emerges Bonus: For any macro fans out there a fellow indie dev (Zardium) offered an additional improvement. From our chat server: Zardium: #define _ [[maybe_unused]] auto CONCAT_MACRO(zz, COUNTER ) Abner: ahh, unique varname. nice one 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 Abner: Yep totally. Although here I was celebrating my removal of a macro… XD 2. More Perfect Unions I’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: typedef struct { Type type; char buf[MAX]; union { time_t time; float dt; date_t date; }; } stamp_t; And if I partially initialize one: stamp_t stamp = { .type = STAMP_TYPE_BLA_BLA }; While it does mean we successfully zeroed out buf (thanks to 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! As I Spongebob-panicked in public Lobsters member accelbread offered a sage revelation: 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 Wait 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: stamp_t stamp = { }; stamp.type = STAMP_TYPE_BLA_BLA; Accelbread went on to say two more things: “Note the C23 zero init whole thing is nothing in the brackets, not 0 in brackets; { 0 } does same thing as before C23” “Also if you control union layout, putting largest member first is safest” Yessir. 3. Enums In C Were Magic (And I Don’t Like It) If I want to customize some dialog box I’ll use bit flags: enum { OPT_AUTOSCROLL = ( 1 << 0 ), OPT_NOCURSOR = ( 1 << 1 ), OPT_CENTERED = ( 1 << 2 ), OPT_NOCLOSE = ( 1 << 3 ), // ... }; [[nodiscard]] bool draw_warning( const char * msg, int opt); Even if we disagree with this approach lots of relevant libraries or projects do it: _ = draw_warning( "you done messed up A-A-RON" , OPT_CENTERED | OPT_NOCLOSE); Of 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. :) I’m kidding. Sigh. Well, I’m growing fearful of sage revelations but this one comes from Modern C by Jens Gustedt: 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. Noooooo! Jens: The property that an an enumeration type is adjusted such that it fits all its constants could have surprising effects sobs Jens: So, it is preferable to specify the underlying integer type explicitly THERE IS HOPE? enum options : int { OPT_AUTOSCROLL = ( 1 << 0 ), OPT_NOCURSOR = ( 1 << 1 ), OPT_CENTERED = ( 1 << 2 ), OPT_NOCLOSE = ( 1 << 3 ), // ... }; It compiles! We shoot. We score. We stick with C23. Epilogue 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.S. If you want to shape this discussion maybe join the Handmade Cities server! -Abner c23 Are We Harold Bloom? →
Comments
No comments yet. Start the discussion.