我的 C23 编码约定

Lobsters Hottest 新闻

摘要

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

<p><a href="https://lobste.rs/s/umdbht/my_c23_conventions">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/10/15 16:23

# 我的 C23 编码约定 来源:https://abner.page/post/c23-conventions-1/ 这是关于我在 2026 年末如何使用现代 C 的三篇迷你系列文章之一。 在 C23 之前,我曾多年深受 *How I Program C*(https://www.youtube.com/watch?v=443UNeGrFoM)的影响,甚至会因为选择了 `-ansi -pedantic` 而不是 C99 而感到内疚。但天哪,老兄,我不再想当末日生存狂了!如今我维护着一个规模不小的项目,用户量也足够多,我应该少给自己找点麻烦。(我还得交房租呢。) 所以,以下三个现代 C 的编码约定,都是我在构建系统中加上 `-std=c23` 后自然而然发展出来的。 ## 1. 用不上?也许吧?(是的) 第一个约定围绕 `[[nodiscard]]` 展开。如果我绘制一个应用窗口或面板,我要求调用者获取刚刚渲染的那个矩形区域的信息: ```c [[nodiscard]] rect_t draw_screen(/* ... */) { rect_t screen_r = { }; // 绘制内容 // ... return screen_r; } ``` 如果调用者没有捕获该矩形以供后续处理,那就是一个业务逻辑 bug。因此 `[[nodiscard]]` 一直是修复 bug 的神器……只不过有极少数情况下,我确实会绘制某些东西,但对返回的矩形*没有任何实际用途*: ```c enum screen_opt popup_options = /* ... */ ; draw_screen(popup_options, /* ... */); // 错误:该死的 [[nodiscard]] ``` Clang 在这里会报错。我本可以捕获这个变量然后永远不用它,但是…… ```c rect_t rect = draw_screen(/* ... */); // 错误:未使用的变量(我启用了 `-Werror`) ``` 在 C23 之前,我可以用一个 `UNUSED()` 宏来解决: ```c #ifndef UNUSED #define UNUSED(x) (void)(x) #endif rect_t rect = draw_screen(/* ... */); UNUSED(rect); // 压制了编译器的警告,但我现在很不开心。 ``` 但现在有了 C23,我已经删掉了这个宏,改用 `[[maybe_unused]]`: ```c [[maybe_unused]] rect_t rect = draw_screen(...); // 搞定 ``` 呼,感觉好多了。我还注意到这种情况到处都是:很多不同的函数返回不同的类型,都标记了 `[[nodiscard]]`。所以对于那少数例外情况,为什么不用这个模式呢: ```c [[maybe_unused]] auto _ = foo(); // 一种新模式就此诞生 ``` 彩蛋:给各位宏爱好者,一位独立开发者朋友(Zardium)提出了一个额外的改进。来自我们的聊天服务器: > Zardium: `#define _ [[maybe_unused]] auto CONCAT_MACRO(zz, __COUNTER__)` > Abner: 啊哈,唯一变量名。不错。 > Zardium: 如果你觉得看起来太奇怪,可以把 `auto` 去掉。那样它就和其他语言里的 `_` 一样,可以被重新定义。 > Abner: 完全可以。虽然我在这里庆祝的是我终于删掉了宏……XD ## 2. 更完美的联合体 我被骗了,被欺骗了:指定初始化器并不会把我的结构体清零,我甚至再也无法信任 `{0}` 了。(简直太过分了。)问题在于,我经常会有类似这种人为构造的时间戳数据类型: ```c typedef struct { Type type; char buf[MAX]; union { time_t time; float dt; date_t date; }; } stamp_t; ``` 而当我部分初始化它时: ```c stamp_t stamp = { .type = STAMP_TYPE_BLA_BLA }; ``` 虽然这确实成功将 `buf` 清零了(多亏了空初始化(https://en.cppreference.com/c/language/initialization#Empty_initialization)),但那些联合体成员是完全不同的东西。亲爱的读者,我曾经犯下一个代价惨重的错误——我以为 `date` 也会被清零! 当我在 Lobsters 上像海绵宝宝一样惊慌失措时,成员 accelbread(https://lobste.rs/~accelbread)给了我一个睿智的启示: > accelbread: 以前 GCC 总是会将整个联合体完全清零,而 Clang 只会清零 time_t 成员,但大约一年前它们互换了行为(如果我没记错的话)。不管怎样,标准只要求 time_t 被清零,而不是整个联合体。GCC 有一个标志可以强制清零整个联合体。另外,C23 的初始化总是初始化整个联合体。 等等,什么?Clang 和 GCC 互换了?!我不打算继续深究了。这一切在我们紧抱 C23 不放之前,都是*极其危险的*: ```c stamp_t stamp = { }; stamp.type = STAMP_TYPE_BLA_BLA; ``` Accelbread 接着又说了两点: 1. *"注意,C23 中将整个东西清零是大括号内什么都不写,而不是写 0;`{ 0 }` 的行为和 C23 之前一样。"* 2. *"另外,如果你能控制联合体的布局,把最大的成员放在最前面是最安全的。"* 收到,长官。 ## 3. C 语言中的枚举曾是魔法(而我不喜欢魔法) 如果我想自定义某个对话框,我会使用位标志: ```c 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); ``` 即使我们不同意这种做法,很多相关的库或项目都这样做: ```c _ = draw_warning("you done messed up A-A-RON", OPT_CENTERED | OPT_NOCLOSE); ``` 当然,错误的位移运算会让任何人陷入麻烦,产生滑稽的数字。谢天谢地,C 枚举底层就是一个普通的 `int`,所以如果我们在枚举中定义了一个放不下的坏常量,编译器会警告我们。 :) 我在开玩笑。叹气。好吧,我越来越害怕那些睿智的启示了,但这次来自 Jens Gustedt 的 *Modern C*(https://gustedt.gitlabpages.inria.fr/modern-c/): > Jens: 如果枚举常量放不下有符号 int,在可能的情况下,枚举类型会被调整以容纳所有枚举常量。 不——要——吧! > Jens: 枚举类型被调整以适应所有常量的特性,可能会产生令人惊讶的效果 *哭泣中* > Jens: 因此,最好显式指定底层整数类型 还有救? ```c enum options : int { OPT_AUTOSCROLL = (1 << 0), OPT_NOCURSOR = (1 << 1), OPT_CENTERED = (1 << 2), OPT_NOCLOSE = (1 << 3), // ... }; ``` 编译通过了!我们开枪了。我们得分了。我们坚守 C23。 ## 尾声 在迷你系列的第二部分中,我将讨论另外三个我通过现代 C 梳理出来的编码约定:首先是更安全的宏使用方式(Zig 的朋友们,请原谅我。) *P.S. 如果你想参与这个讨论,不妨加入(https://handmadecities.com/chat)Handmade Cities 服务器!* —Abner

相似文章

C++26:减少未定义行为

Lobsters Hottest

C++26 引入了减少未定义行为的更改,特别是使删除指向不完整类型的指针成为格式错误,从而提高了程序安全性。

Dependable C

Lobsters Hottest

一个全面的资源,提供编写可靠、安全的C代码的指南和建议,涵盖未定义行为、内存模型以及特定版本的注意事项。

解析C语言中类型推断声明之险

Lobsters Hottest

这篇博文探讨了C23中涉及`auto`作为类型推断说明符或存储类说明符的解析歧义,展示了当`x`是typedef时,GCC和Clang在解析如`auto x = 67;`这样的声明上的分歧,以及属性如何使情况复杂化。