1. 什么是属性
在 C++ 中,属性 是一种给编译器下指令的机制。它允许程序员告诉编译器某些额外的信息,比如“不要警告我”、“这段代码可能会失败”或者“把这个函数放到某个特定的段”。
属性的标准写法是用 两个方括号 [[ ... ]] 括起来:
在 C++17 之前,大家最常使用的属性是 [[deprecated]],用来标记某个函数或变量已经被弃用,如果别人还在用,编译器就会发出警告。
假设你写了一个旧版本的函数,后来发现更好的实现方法,但为了兼容性又不能立刻删除旧函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream>
[[deprecated]] void oldFunc() {}
[[deprecated("请使用新的 FastCalculate() 函数")]] int CalculateOldWay() { return 100; }
int FastCalculate() { return 100; }
int main() { int result = CalculateOldWay(); std::cout << "result = " << result << std::endl; return 0; }
|
编译时你会看到警告:
1
| warning: 'CalculateOldWay' is deprecated: 请使用新的 FastCalculate() 函数
|
2. C++17 新增属性
C++17 主要引入了三个新特性:
[[nodiscard]]:防止忽略返回值(非常重要!)。
[[maybe_unused]]:消除未使用变量的警告。
[[fallthrough]]:虽然属于 switch 语句优化,但也算属性相关。但这部分通常归类于语句属性,我们这里重点讲前两个最常用的,外加对所有属性的通用性增强。
2.1 [[nodiscard]]
想象你写了一个检查密码是否正确的函数,它返回 true 或 false。但是,调用这个函数的人可能忘了写 if 判断,直接调用了就不管了。这就意味着程序不管密码对错都继续往下跑,这会产生严重的安全漏洞!
解决方案就是在函数返回类型前面加上 [[nodiscard]]。如果程序员调用了这个函数却没有处理返回值(比如没有赋值给变量,也没有放在 if 里),编译器会直接报错或发出严重警告。
2.1.1 标记普通函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <iostream> #include <string>
[[nodiscard]] bool checkFileExists(const std::string& filename) { std::cout << "正在检查文件: " << filename << "..." << std::endl; return false; }
enum class [[nodiscard]] ErrorCode { OK, NetworkError, FileSystemError };
ErrorCode doSomething() { return ErrorCode::NetworkError; }
int main() { std::cout << "=== 测试 [[nodiscard]] ===" << std::endl;
if (checkFileExists("data.txt")) { std::cout << "文件找到了!" << std::endl; } else { std::cout << "文件不存在。" << std::endl; }
std::cout << "\n尝试不处理返回值..." << std::endl;
checkFileExists("ignore_me.txt");
doSomething();
return 0; }
|
如果你在简单的编译环境中运行可能看不到报错,建议在 GCC/Clang 中加上 -Wall -Werror 或在 MSVC 中使用 /W4 来体验效果。
[[nodiscard]] 属性在C++20 扩展中可以携带消息,但很多编译器(如 GCC 9+, Clang 10+)在 C++17 模式下也支持给 nodiscard 加理由字符串,这非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
[[nodiscard("转账操作必须检查结果,防止资金丢失")]] int transferMoney() { return 0; }
int main() { transferMoney(); return 0; }
|
2.1.2 标记构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <iostream>
class ImportantResource { public: [[nodiscard]] ImportantResource() { std::cout << "重要资源已创建\n"; }
~ImportantResource() { std::cout << "重要资源已销毁\n"; } };
void testResource() { ImportantResource(); ImportantResource res; }
int main() { testResource(); return 0; }
|
2.2 [[maybe_unused]]
在开发过程中,我们经常会遇到变量定义了但没用的情况。
- 比如你写了一个函数,参数
int flag 为了将来扩展预留的,现在还没用到。
- 或者你在
Debug 模式下定义了变量,但在 Release 模式下被宏优化掉了。
编译器看到未使用的变量会很唠叨,输出一堆黄色的警告信息。这些警告多了,反而会淹没真正严重的错误。如果不想这样就可以在变量、函数、参数等声明前加上 [[maybe_unused]],编译器会对这个变量睁一只眼闭一只眼,不再报未使用的警告。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream>
void oldStyleFunc(int a, int b) { (void)a; std::cout << "b 的值是: " << b << std::endl; }
[[maybe_unused]] void newStyleFunc(int a, int b) { [[maybe_unused]] int x = 10; std::cout << "b 的值是: " << b << std::endl; }
void processRequest([[maybe_unused]] bool debugMode) { std::cout << "处理请求中..." << std::endl;
#ifdef DEBUG if (debugMode) { std::cout << "Debug 模式开启!" << std::endl; } #endif }
int main() { std::cout << "=== 测试 [[maybe_unused]] ===" << std::endl;
newStyleFunc(100, 200); processRequest(true);
return 0; }
|
2.3 [[fallthrough]]
在 C++(以及 C 语言)的传统语法中,switch 语句有一个著名的特性叫做 “直落”。它的意思是:如果在一个 case 分支的代码执行完后,没有写 break 语句,程序会直接穿过这个 case 的限制,继续执行下一个 case 里面的代码。也就是说直落是具有两面性的:
- 优点:有时我们希望不同的条件共用一段逻辑,写起来很方便。
- 缺点:大多数时候,忘记写
break 是严重的程序 Bug(这叫“逻辑穿透”),而且很难发现。
在 C++17 之前,编译器看到没用 break 就穿透到下一个case的情况,通常会发出警告(Warning)。但程序员如果是故意想穿透的,这个警告就很烦人。为了解决这个问题,C++17 引入了 [[fallthrough]]。它的作用就是告诉编译器:“不用警告,我是故意这么写的!”
[[fallthrough]] 必须作为一个单独的语句出现,并且放在非空的 case 代码块的末尾(最后一句)。
1 2 3 4 5 6 7 8 9
| switch (变量) { case 1: [[fallthrough]]; case 2: break; }
|
使用 [[fallthrough]] 时,违反以下任意一条,编译器都会报错:
- 必须是空的:它后面必须是分号
;,不能跟其他代码。
- 只能在 case 内部:不能随便放在别的地方。
- 对应的这个 case 不能是空的:这是最容易踩的坑! 你不能在一个本来就没写代码的
case 后面直接加 [[fallthrough]],必须先写点代码(哪怕只是一个注释或空语句,但标准要求通常得有实际执行逻辑)。
假设我们有这样一个需求:检查成绩等级。
- 如果是 A,打印“优秀”。
- 如果是 B,打印“良好”。
- 如果是 C 或 D,都打印“及格”。
- 否则“不及格”。
对于 C 和 D,我们就可以用穿透。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream>
void checkGrade(char grade) { std::cout << "检查成绩: " << grade << " -> ";
switch (grade) { case 'A': std::cout << "优秀"; break;
case 'B': std::cout << "良好"; break;
case 'C': std::cout << "勉强及格 (C)"; [[fallthrough]];
case 'D': std::cout << " 和 (D) -> 最终结论:及格" << std::endl; break;
default: std::cout << "不及格" << std::endl; break; } }
int main() { checkGrade('A'); checkGrade('C'); checkGrade('D'); return 0; }
|
在 case 'C' 的末尾,我们加了 [[fallthrough]];。编译器看到这个标记,就知道我们故意没有写 break,从而不会发出 “你可能忘记写 break 了” 的警告。
在编写库代码、API 接口时,尽量使用这些属性,这样能给你的代码使用者更好的指导和保护。