My C23 Conventions

Lobsters Hottest News

Summary

作者分享了在 C23 标准下采用的三种现代 C 编码约定,包括使用 [[nodiscard]] 与 [[maybe_unused]] 处理未使用返回值、C23 中 union 初始化行为的变化,以及借助 auto 简化代码的新模式。

<p><a href="https://lobste.rs/s/umdbht/my_c23_conventions">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 10/15/26, 04:23 PM

# My C23 Conventions Source: [https://abner.page/post/c23-conventions-1/](https://abner.page/post/c23-conventions-1/) 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](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\.\) 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 imrpovement\. 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](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\! As I Spongebob\-panicked in public Lobsters member[accelbread](https://lobste.rs/~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: 1. *“Note the C23 zero init whole thing is nothing in the brackets, not 0 in brackets; \{ 0 \} does same thing as before C23”* 2. *“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](https://gustedt.gitlabpages.inria.fr/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](https://handmadecities.com/chat)the Handmade Cities server\!* \-Abner

Similar Articles

C++26: Reducing undefined behaviour

Lobsters Hottest

C++26 introduces changes to reduce undefined behavior, notably making it ill-formed to delete a pointer to an incomplete type, improving program safety.

Dependable C

Lobsters Hottest

A comprehensive resource providing guidelines and recommendations for writing dependable, safe C code, covering undefined behavior, memory model, and version-specific advice.

the perils of parsing type inference declarations in c

Lobsters Hottest

This blog post explores parsing ambiguities in C23 involving `auto` as a type inference specifier or storage-class specifier, showing how GCC and Clang disagree on parsing declarations like `auto x = 67;` when `x` is a typedef, and how attributes complicate the situation.