我的 C23 编码约定
摘要
作者分享了在 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语言中在C++中仍然无法工作的构造——以及一些已发生变化的构造
一篇更新经典调查的博文,关于C语言中在C++中无法工作的构造,涵盖了C++20和C23标准中影响兼容性的变化。
深入C语言:详解2016年事实标准
本文深入探讨C编程语言,重点介绍其2016年的事实标准。
C++26:减少未定义行为
C++26 引入了减少未定义行为的更改,特别是使删除指向不完整类型的指针成为格式错误,从而提高了程序安全性。
Dependable C
一个全面的资源,提供编写可靠、安全的C代码的指南和建议,涵盖未定义行为、内存模型以及特定版本的注意事项。
解析C语言中类型推断声明之险
这篇博文探讨了C23中涉及`auto`作为类型推断说明符或存储类说明符的解析歧义,展示了当`x`是typedef时,GCC和Clang在解析如`auto x = 67;`这样的声明上的分歧,以及属性如何使情况复杂化。