属性增强

1. 什么是属性

在 C++ 中,属性 是一种给编译器下指令的机制。它允许程序员告诉编译器某些额外的信息,比如“不要警告我”、“这段代码可能会失败”或者“把这个函数放到某个特定的段”。

属性的标准写法是用 两个方括号 [[ ... ]] 括起来:

1
[[标签]] 代码

在 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 主要引入了三个新特性:

  1. [[nodiscard]]:防止忽略返回值(非常重要!)。
  2. [[maybe_unused]]:消除未使用变量的警告。
  3. [[fallthrough]]:虽然属于 switch 语句优化,但也算属性相关。但这部分通常归类于语句属性,我们这里重点讲前两个最常用的,外加对所有属性的通用性增强。

2.1 [[nodiscard]]

想象你写了一个检查密码是否正确的函数,它返回 truefalse。但是,调用这个函数的人可能忘了写 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]] 告诉调用者:你必须检查返回值!
[[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;

// 场景 1:正确用法 - 处理了返回值
if (checkFileExists("data.txt"))
{
std::cout << "文件找到了!" << std::endl;
}
else
{
std::cout << "文件不存在。" << std::endl;
}

std::cout << "\n尝试不处理返回值..." << std::endl;

// 场景 2:错误用法 - 忽略了返回值
// 如果你的编译器支持 C++17 且开启了相应警告级别,下面这行代码会报错或警告
checkFileExists("ignore_me.txt");

// 场景 3:忽略枚举示例
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
// 编译器可能会提示:warning: ignoring return value of 'int transferMoney()', 
// declared with attribute 'nodiscard': '转账操作必须检查结果,防止资金丢失'
[[nodiscard("转账操作必须检查结果,防止资金丢失")]]
int transferMoney()
{
// ... 执行转账
return 0; // 0 成功, -1 失败
}

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; // 告诉编译器:我知道 a 没用,别吵
std::cout << "b 的值是: " << b << std::endl;
}

// C++17 写法:优雅得多
[[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
// 如果没有定义 DEBUG,上面那个 debugMode 参数就没用
// 加上 [[maybe_unused]] 就不会报警告了
}

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 里面的代码。也就是说直落是具有两面性的:

  1. 优点:有时我们希望不同的条件共用一段逻辑,写起来很方便。
  2. 缺点:大多数时候,忘记写 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]] 时,违反以下任意一条,编译器都会报错:

  1. 必须是空的:它后面必须是分号 ;,不能跟其他代码。
  2. 只能在 case 内部:不能随便放在别的地方。
  3. 对应的这个 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':
// 这里会被 C 穿透进来
std::cout << " 和 (D) -> 最终结论:及格" << std::endl;
break;

default:
std::cout << "不及格" << std::endl;
break;
}
}

int main()
{
checkGrade('A'); // 输出: 优秀
checkGrade('C'); // 输出: 勉强及格 (C) 和 (D) -> 最终结论:及格
checkGrade('D'); // 输出: 和 (D) -> 最终结论:及格
return 0;
}

case 'C' 的末尾,我们加了 [[fallthrough]];。编译器看到这个标记,就知道我们故意没有写 break,从而不会发出 “你可能忘记写 break 了” 的警告。

在编写库代码、API 接口时,尽量使用这些属性,这样能给你的代码使用者更好的指导和保护。