我以前挺怕 warning 的。
不是怕看到红字。
是怕它变多。
一旦 warning 变成背景噪声,你就会下意识忽略它。
然后真正危险的那条混进来。
等你在事故复盘里再见到它,就晚了。
先把话说清:warning 到底在提醒什么
warning 其实就是一种“静态检查”。
它不是运行时崩溃。
它在说:这里像 bug。
这里面有一个现实。
很多“看起来像错”的代码。
在工程里是你故意写的。
比如故意不用某个参数。
比如故意让 switch 贯穿到下一个分支。
所以我想要的不是“关掉它”。
而是能明确告诉编译器:我没忘。
我就是这么写的。
再说 [[...]]:它是什么
[[...]] 叫 attribute(属性)。
你可以把它当成“给编译器看的标签”。
标签不改变你的业务逻辑。
它主要影响两件事。
一是诊断(warning / error)。
二是工具链(静态分析、格式化、文档)。
先记住它长什么样就够了。
[[maybe_unused]] int x = 0;
它表达的意思很直白:
这东西可能会用,也可能不会用。
先看一个坑:switch 漏 break
int type = 1;
switch (type) {
case 1:
handle_a();
case 2:
handle_b();
break;
}
这段代码最麻烦的点是:看起来很像“写完整了”。
但 type == 1 时会掉进 case 2。
有时候你会得到 A+B。
有时候你会把状态推进两次。
旧办法:写注释“我就是故意的”
switch (type) {
case 1:
handle_a();
// fall through
case 2:
handle_b();
break;
}
这对人类读者很友好。
但对编译器不一定。
有些编译器会认某些注释写法。
有些不会。
坑点反例:你以为写了注释就万事大吉
case 1:
handle_a();
// fallthrough!!!
case 2:
handle_b();
break;
你对人说“我故意的”。
编译器可能只看到:我不认识这个注释。
它继续报警。
你就更想把警告关掉。
我真正想要的性质:意图要可机器检查
我不想靠同事的记忆力。
也不想靠“注释的拼写”。
我想要一个标准写法。
让编译器在该闭嘴时闭嘴。
没闭嘴的时候,我就知道:这里可能真的漏了 break。
C++17:[[fallthrough]],给 switch 一个“签字”
switch (type) {
case 1:
handle_a();
[[fallthrough]];
case 2:
handle_b();
break;
}
[[fallthrough]]; 是一个空语句。
它不改变运行时行为。
它只是在说:我知道我要贯穿,这次是故意的。
[[fallthrough]] 放哪最稳
case 1:
handle_a();
[[fallthrough]];
case 2:
handle_b();
break;
把它放在 case 的末尾。
紧挨着下一个 case。
中间别夹别的语句。
你写它的目的就是签字。
坑点反例:写了 [[fallthrough]] 但其实不会贯穿
case 1:
handle_a();
[[fallthrough]];
break;
case 2:
handle_b();
break;
这段代码不会贯穿。
你却写了“我会贯穿”的签字。
好的编译器通常会在这里继续提醒你。
因为你的意图和代码行为不一致。
题外话:C++17 之前大家怎么“签字”
很多工程在标准化之前就已经在做这件事了。
GCC/Clang 有自己的方言属性。
case 1:
handle_a();
__attribute__((fallthrough));
case 2:
handle_b();
break;
能用。
代价也很明显。
代码开始写进“编译器方言”。
跨平台就得加宏。
你会越来越像在维护一个小型“方言翻译器”。
横向对比:为什么别的语言更少踩这个坑
很多语言的 switch 默认不允许贯穿。
想贯穿你得显式写关键字。
这其实就是 [[fallthrough]] 想补回来的那条约束。
只不过 C/C++ 的历史包袱太大。
不能直接改默认语义。
所以它只能用“诊断+签字”的方式补救。
再看一个常见 warning:unused parameter
void on_event(int fd, int flags) {
handle_global_event();
}
你不是忘了 fd、flags。
你只是现在用不到。
但编译器不知道。
它只看到:参数没用。
C 和旧 C++ 的老习俗:(void)x
void on_event(int fd, int flags) {
(void)fd;
(void)flags;
handle_global_event();
}
这句不是业务逻辑。
它是一句“别报警”。
写多了以后,代码里会出现大量这种噪声。
C++17:[[maybe_unused]]
void on_event([[maybe_unused]] int fd,
[[maybe_unused]] int flags) {
handle_global_event();
}
意思也很简单:
可能会用,也可能不会用。
例子:只在 debug 下用到的变量
void f() {
[[maybe_unused]] int debug_counter = 0;
do_work();
}
你在 release 模式可能把 debug_counter 相关代码裁掉。
这时候它就会变成未使用。
[[maybe_unused]] 是一个明确的标记。
告诉读者:这个变量就是拿来调试用的。
题外话:GCC/Clang 的 unused 方言
早年跨平台项目里,你会看到这种写法。
void on_event(int fd __attribute__((unused))) {
handle_global_event();
}
它能解决问题。
但它把“消音开关”绑在了特定编译器上。
你要兼容别家,就得再写宏。
标准属性的价值就在这。
它把方言统一成普通话。
坑点反例:[[maybe_unused]] 也能把真正的 bug 一起埋掉
int calc();
void g() {
[[maybe_unused]] int r = calc();
do_work();
}
如果 r 本来就该参与逻辑。
那你加上 [[maybe_unused]] 等于给自己戴了耳塞。
编译器不提醒你了。
你也更难在 review 里发现“你是不是忘用结果”。
我自己的边界:只给“接口约束”和“条件编译差异”签字
当参数是接口规定的。
或者某个平台才会用到。
我才会用 [[maybe_unused]]。
其它情况。
能删就删。
能补上逻辑就补上逻辑。
别把它当万能消音器。
关键结论
[[fallthrough]] 是给 switch 的签字。
[[maybe_unused]] 是给“这次不用”的签字。
你签字越清楚。
警告就越像信号。
你签字越随意。
警告就越像噪声。