6. 代码标注
# 6. 代码标注
代码标注可能并非C++广为人知的特性。然而,它对于向编译器传达额外信息以及帮助其他程序员理解代码很有帮助。自C++11起,有了一种标准的指定代码标注的方式。
在C++17中,我们又新增了更多实用的代码标注。在本章中,你将了解到:
- C++中的代码标注是什么;
- 特定厂商的代码标注与标准代码标注的区别;
- 在哪些情况下代码标注很有用;
- C++11和C++14中的代码标注;
- C++17中的新增代码标注。
# 为什么我们需要代码标注?
你在代码中使用过declspec
、attribute
或#pragma
指令吗?例如:
// 设置对齐方式
struct S { short f[3]; } attribute ((aligned (8)));
// 该函数不会返回
void fatal () attribute ((noreturn));
// 在MSVC中用于DLL导入/导出
#if COMPILING_DLL
#define DLLEXPORT declspec(dllexport)
#else
#define DLLEXPORT declspec(dllimport)
#endif
2
3
4
5
6
7
8
9
10
这些是现有的特定于编译器的代码标注形式。那么,代码标注究竟是什么呢?
代码标注是一种额外的信息,编译器可利用它生成代码。它可用于优化或特定的代码生成(比如DLL相关操作、OpenMP等)。此外,代码标注能让你编写更具表现力的语法,并帮助其他开发者理解代码。
与诸如C#之类的其他语言不同,在C++中,编译器固定了元信息系统,你无法添加用户自定义代码标注。而在C#中,你可以从System.Attribute
派生。
现代C++代码标注的优势是什么呢?
自C++11起,越来越多的代码标注实现了标准化,可在不同编译器上通用。我们正从特定于编译器的注释转向标准形式。你无需学习各种标注语法,而是能够编写通用且行为一致的代码。
在下一节中,你将了解在C++11之前代码标注是如何使用的。
# C++11之前
在C++98/03时代,每个编译器都引入了各自的代码标注集,且通常使用不同的关键字。
你经常会看到代码中到处散布着#pragma
、declspec
、attribute
。
以下是GCC/Clang和MSVC的常见语法列表:
# GCC特定代码标注
GCC使用attribute ((attr_name))
形式的注释。例如:
int square (int ) attribute ((pure)); // 纯函数
文档:
# MSVC特定属性
微软主要使用declspec
关键字作为各种编译器扩展的语法。相关文档见:__declspec
(opens new window)(微软文档) 。
declspec(deprecated) void LegacyCode() { }
# Clang特定属性
Clang易于定制,能支持不同类型的代码标注,更多内容可查看其文档。大多数GCC的代码标注在Clang中也能使用。
相关文档见:《Clang中的属性 —— Clang文档 (opens new window)》。
# C++11和C++14中的代码标注
C++11朝着减少使用特定厂商语法的方向迈出了一大步。通过引入标准格式,我们可以将许多特定于编译器的属性整合到通用集合中。
C++11为在代码中指定代码标注提供了更简洁的格式。基本语法是[[attr]]
或[[namespace::attr]]
。
你几乎可以在任何内容上使用[[attr]]
,如类型、函数、枚举等等。例如:
[[attrib_name]] void foo() { } // 用于函数
struct [[deprecated]] OldStruct { } // 用于结构体
2
在C++11中,有以下属性:
[[noreturn]]
:它告诉编译器控制流不会返回给调用者。例如:[[noreturn]] void terminate() noexcept;
- 像
std::abort
或std::exit
这样的函数也用这个标注标记。
[[carries_dependency]]
:表明在release-consume
的std::memory_order
中的依赖链在函数内外传播,这能让编译器跳过不必要的内存屏障指令。主要用于优化多线程代码以及在使用不同内存模型时。
C++14添加了:
[[deprecated]]
和[[deprecated("reason")]]
:标记了这个标注的代码会被编译器报告。你可以设置弃用原因。
[[deprecated]]
的示例:
[[deprecated("use AwesomeFunc instead")]] void GoodFunc() { }
// 在某处调用:
GoodFunc();
2
3
GCC会报告以下警告:
warning: 'void GoodFunc()' is deprecated: use AwesomeFunc instead [-Wdeprecated-declarations]
你已经对旧的方法以及C++11/14中的新方式有了一些了解…… 那么C++17又有什么新变化呢?
# C++17新增内容
C++17引入了三个新的标准代码标注:
[[fallthrough]]
[[nodiscard]]
[[maybe_unused]]
# 额外信息
这些新代码标注在P0188 (opens new window)和P0068 (opens new window)(论证)中被指定。
此外,还有三个支持特性:
- 命名空间和枚举器的代码标注
- 忽略未知代码标注
- 无需重复使用代码标注命名空间
让我们先来了解一下这些新代码标注。
# [[fallthrough]]
标注
该标注表明switch
语句中的穿透(fall-through)是有意为之,不应为此发出警告。
switch (c) {
case 'a':
f(); // 警告!穿透可能是程序员的错误
case 'b':
g();
[[fallthrough]]; // 警告被抑制,穿透是允许的
case 'c':
h();
}
2
3
4
5
6
7
8
9
有了这个标注,编译器可以理解程序员的意图。而且它比使用注释更具可读性。
# [[maybe_unused]]
标注
该属性用于抑制编译器关于未使用实体的警告:
static void impl1() { ... } // 当函数未被调用时,编译器可能会发出警告
[[maybe_unused]] static void impl2() { ... } // 警告被抑制
void foo() {
int x = 42; // 如果之后x未被使用,编译器可能会发出警告
[[maybe_unused]] int y = 42; // 对y的警告被抑制
}
2
3
4
5
6
7
当某些变量和函数仅在调试路径中使用时,这种特性很有帮助。例如在assert()
宏中:
void doSomething(std::string_view a, std::string_view b) {
assert(a.size() < b.size());
}
2
3
如果之后a
或b
在这个函数中未被使用,那么编译器会在发布版本的构建中生成警告。使用[[maybe_unused]]
标记给定的参数可以解决这个警告。
# [[nodiscard]]
标注
[[nodiscard]]
可以应用于函数或类型声明,以标记返回值的重要性:
[[nodiscard]] int Compute();
void Test() {
Compute(); // 警告!带有nodiscard标注的函数返回值被丢弃
}
2
3
4
5
如果你忘记将结果赋值给变量,编译器应该发出警告。
这意味着你可以强制用户处理错误。例如,如果你忘记使用new
或std::async()
的返回值会发生什么呢?
此外,该标注还可以应用于类型。一个用例可能是错误码:
enum class [[nodiscard]] ErrorCode {
OK,
Fatal,
System,
FileIssue
};
ErrorCode OpenFile(std::string_view fileName);
ErrorCode SendEmail(std::string_view sendto,
std::string_view text);
ErrorCode SystemCall(std::string_view text);
2
3
4
5
6
7
8
9
10
11
现在,每次调用这些函数时,你都“被迫”检查返回值。对于重要的函数,检查返回码可能至关重要,使用[[nodiscard]]
可以帮你避免一些错误。
你可能还会问,“不使用”返回值是什么意思呢?
在标准中,它被定义为“丢弃值表达式 (opens new window)” 。这意味着你调用一个函数仅仅是为了它的副作用。换句话说,周围没有if
语句或赋值表达式。在这种情况下,当一个类型被标记为[[nodiscard]]
时,编译器会被鼓励报告一个警告。
然而,要抑制这个警告,你可以显式地将返回值转换为void
,或者使用[[maybe_unused]]
:
[[nodiscard]] int Compute();
void Test() {
static_cast<void>(Compute()); // 没问题...
[[maybe_unused]] auto ret = Compute();
}
2
3
4
5
6
此外,在C++20中,标准库会在一些地方使用[[nodiscard]]
,比如:operator new
、std::async()
、std::allocate()
、std::launder()
和std::empty()
。
这个特性已经通过P0600 (opens new window) 合并到C++20中。
C++20的第二个新增内容是[[nodiscard("reason")]]
,详见P1301 (opens new window) 。这允许你指定不使用返回值可能会产生问题的原因,例如,某些资源泄漏。
# 命名空间和枚举器的标注
C++11中引入属性的目的是能够将它们应用到所有合理的地方,如类、函数、变量、类型定义、模板、枚举等等。但在规范中有一个问题,当属性应用于命名空间或枚举器时会受到限制。
C++17修复了这个问题。现在我们可以这样写:
namespace [[deprecated("use BetterUtils")]] GoodUtils {
void DoStuff() { }
}
namespace BetterUtils {
void DoStuff() { }
}
// 使用:
GoodUtils::DoStuff();
2
3
4
5
6
7
8
9
Clang会报告:
warning: 'GoodUtils' is deprecated: use BetterUtils [-Wdeprecated-declarations]
另一个例子是在枚举器上使用deprecated
标注:
enum class ColorModes {
RGB [[deprecated("use RGB8")]],
RGBA [[deprecated("use RGBA8")]],
RGB8,
RGBA8
};
// 使用:
auto colMode = ColorModes::RGBA;
2
3
4
5
6
7
8
在GCC下,我们会得到:
warning: 'RGBA' is deprecated: use RGBA8 [-Wdeprecated-declarations]
# 额外信息
该变更在N4266 (opens new window)(措辞)和N4196 (opens new window)(论证)中被描述。
# 忽略未知标注
这个特性主要是为了明确规范。
在C++17之前,如果你尝试使用一些特定于某个编译器的标注,在另一个不支持该标注的编译器中编译时,甚至可能会出错。现在,编译器会忽略标注规范,并且不会报告任何内容(或者仅给出一个警告)。这在标准中之前没有提及,需要进行明确。
// 不支持MyCompilerSpecificNamespace的编译器将忽略这个属性
[[MyCompilerSpecificNamespace::do_special_thing]]
void foo();
2
3
例如在GCC 7.1中会有一个警告:
warning: 'MyCompilerSpecificNamespace::do_special_thing'
scoped attribute directive ignored [-Wattributes]
void foo();
2
3
# 额外信息
该变更在P0283R2 (opens new window)(措辞)和P0283R1 (opens new window)(论证)中被描述。
# 无需重复使用标注命名空间
这个特性简化了需要使用多个标注的情况,比如:
void f() {
[[rpr::kernel, rpr::target(cpu,gpu)]] // 有重复
doTask();
}
2
3
4
提议的修改:
void f() {
[[using rpr: kernel, target(cpu,gpu)]]
doTask();
}
2
3
4
在构建将这种带注释的代码自动转换为不同编程模型的工具时,这种简化可能会有所帮助。
# 额外信息
该变更在P0028R4 (opens new window) 中被描述。
# 章节总结
C++17中可用的标注:
属性 | 描述 |
---|---|
[[noreturn]] | 函数不会返回给调用者 |
[[carries_dependency]] | 关于依赖链的额外信息 |
[[deprecated]] | 实体已被弃用 |
[[deprecated("reason")]] | 提供关于弃用的额外信息 |
[[fallthrough]] | 表明switch 语句中的穿透是有意为之 |
[[nodiscard]] | 如果返回值被丢弃,会生成一个警告 |
[[maybe_unused]] | 代码中某个实体可能未被使用 |
每个编译器厂商都可以指定自己的标注和注释语法。在现代C++中,ISO委员会试图提取通用部分并将其标准化为[[attributes]]
。
Bjarne Stroustrup的C++11常见问题解答 (opens new window) 中有一段关于建议用法的相关引用:
“人们有理由担心属性会被用于创建语言方言。建议仅将属性用于控制那些不影响程序含义,但可能有助于检测错误(例如[[noreturn]]
)或帮助优化器(例如[[carries_dependency]]
)的方面。”
# 编译器支持
特性 | GCC | Clang | MSVC |
---|---|---|---|
[[fallthrough]] | 7.0 | 3.9 | 15.0 |
[[nodiscard]] | 7.0 | 3.9 | 15.3 |
[[maybe_unused]] | 7.0 | 3.9 | 15.3 |
命名空间和枚举器的标注 | 4.9/6¹⁶ | 3.4 | 14.0 |
忽略未知属性 | 所有版本 | 3.9 | 14.0 |
无需重复使用标注命名空间 | 7.0 | 3.9 | 15.3 |
上述所有编译器也都支持C++11/14的标注。
批注 ¹⁶ GCC 4.9(命名空间)/ GCC 6(枚举)