C++17 详解 15

本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。

5. 标准属性(attribute)

代码注解——属性——可能不是 C++ 最为人所知的特性。但是,属性却可能便于对编译器和其他程序员传达附加信息。C++11 起有了标准方式来声明属性。C++17 里你将能获得更多属性相关的有用的附加信息。

本章你将学到:

  • C++ 里属性是什么
  • 厂商特有的代码注解 V.S. 标准格式
  • 什么情况下属性是有用的
  • C++11 和 C++14 属性
  • C++17 新添加的属性

5.1 我们为什么需要属性?

你是否曾在自己的代码里用过 __declspec__attribute#pragma 指示符?

比如:

1
2
3
4
5
// 设置对齐
struct S { short f[3]; } __attribute__ ((aligned (8)));

// 此函数不会返回
void fatal () __attribute__ ((noreturn));

或是在 MSVC 里导入/导出 DLL:

1
2
3
4
5
#if COMPILING_DLL
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllimport)
#endif

这些都是现存的编译器特有的属性/注解格式。

所以属性是什么?

一个属性是一段编译器可以用来生成代码的附加信息。它可能被用于优化代码或一些特定的代码生成(比如 DLL 之类,OpenMP,等等)。此外,注解(译注:等同于属性)能让你写出更有表达力的代码,并帮助其他开发者理解代码。

与 C# 等其它语言不同,C++ 里编译器确定了元信息,你不能自定义属性。C# 里你可以“继承” System.Attribute。

现代 C++ 的属性最好的点在哪呢?

现在 C++ 里,我们有了越来越多的标准化了的属性,它们能在其它编译器里正常工作。所以我们要从编译器特有的注解转移到标准化格式上来。

下一节你将看到 C++11 之前是怎么利用属性来工作的。

5.2 C++11 之前

每个编译器都引入了自己的注解集合,通常都使用了不同的关键字。

你可能会很经常地看到通篇代码里散布着 #pragma__declspec__attribute 这样的代码。

这里是一份 GCC/Clang 和 MSVC 里常见注解语法的列表:

GCC 特有属性

GCC 以 __attribute__((attr_name)) 的格式使用注解。比如:

1
int square (int) __attribute__ ((pure)); // pure function

文档:

MSVC 特有属性

微软大多使用 __declspec 关键字作为各种编译器扩展的语法。文档在这里:__declspec

1
__declspec(deprecated) void LegacyCode() { }

Clang 特有属性

Clang 因为方便定制,能支持不同的注解类型,可以查阅文档获取更多信息。大多数 GCC 注解都可以在 Clang 下工作。

文档在这里:属性手册

5.3 C++11/14 属性

C++11 在最小化厂商特有语法需求方面迈出了一大步。通过引入标准格式,我们能把大量编译器特有属性转换到标准集合中。

C++11 提供了一种更美观的形式以在我们的代码中声明属性。

基本语法就是 [[attr]] 或 [[namespace::attr]]。

你可以在几乎任何事物上使用 [[attr]]:类型、函数、枚举,等等等等。

比如:

1
2
[[abc]] void foo()
{}

C++11 里有如下属性:

[[noreturn]]:

不会返回的函数。如果带有此属性的函数返回,行为未定义。

  • 比如 [[noreturn]] void terminate() noexcept
  • std::abortstd::exit 函数均以此属性标记。

[[carries_dependency]]:

Indicates that the dependency chain in release-consume std::memory_order propagates in and out
of the function, which allows the compiler to skip unnecessary memory fence instructions. Mostly
to help to optimise multi-threaded code and when using different memory models.

译注:memory order 相关,用于无锁编程,译者一窍不通,不译。

C++14 添加的:

[[deprecated]] 和 [[deprecated(“reason”)]]:

使用被此属性标记的代码将被编译器报告。你可以设置为什么的“理由”。

[[deprecated]] 示例:

1
2
3
4
[[deprecated("use AwesomeFunc instead")]] void GoodFunc() { }

// 调用:
GoodFunc();

GCC 可能会报出 warning:

1
2
warning: 'void GoodFunc()' is deprecated: use AwesomeFunc instead
[-Wdeprecated-declarations]

现在你知道了一点老方案,C++11/14 里的新方式……那 C++17 里又干了什么呢?

5.4 C++17 新增

C++17 里我们有了另外三种标准属性:

  • [[fallthrough]]
  • [[nodiscard]]
  • [[maybe_unused]]

还添加了三种支持的特性。

扩展:新属性在 P0188P0068(讨论) 中规范。

先来看新属性:

[[fallthrough]] 属性

指示 switch 语句中的直落是有意的,相关的 warning 不应被发出。

1
2
3
4
5
6
7
8
9
switch (c) {
case 'a':
f(); // Warning! 直落可能是一个人为错误
case 'b':
g();
[[fallthrough]]; // Warning 被抑制,直落 OK
case 'c':
h();
}

通过此属性,编译器能理解程序员的意图。相比使用注释,属性更具有可读性。

[[maybe_unused]] 属性

通过声明为 [[maybe_unused]] 来抑制编译器对未使用实体的 warning。

1
2
3
4
5
6
7
static void impl1() { ... } // 编译器可能会对此发出警告
[[maybe_unused]] static void impl2() { ... } // Warning 被抑制

void foo() {
int x = 42; // 编译器可能会对此发出警告
[[maybe_unused]] int y = 42; // Warning 被抑制
}

[[nodiscard]] 属性

[[nodiscard]] 可被用于函数或类型声明以标记返回值的重要性:

1
2
3
4
[[nodiscard]] int Compute();
void Test() {
Compute(); // Warning! nodiscard 函数的返回值被舍弃了。
}

如果你忘了将结果赋值给变量,编译器就会抛出 warning。

这意味着你可以强制用户处理错误。比如:如果你忘了使用 newstd::async() 的返回值会发生什么?

另外,此属性可被用于类型。一种使用情景是错误码类型:

1
2
3
4
5
6
7
8
9
10
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);

现在每次你调用这些函数时你都会被“强制”检查返回值。对重要函数来说检查返回值可能是至关重要的,使用 [[nodiscard]] 可能会让你少写几个 bug。

你也可能会问“使用”返回值是什么意思?

标准里它被定义为弃值表达式,所以如果你的函数调用只产生副作用(没有被用作 if 语句或赋值),将鼓励编译器报告一个 warning。

为了抑制这种 warning 你可以显式地把返回值转换 void,或使用 [[maybe_unused]]:

1
2
3
4
5
6
[[nodiscard]] int Compute();
void Test() {
static_cast<void>(Compute());

[[maybe_unused]] auto ret = Compute();
}

另外,C++20 标准库会把 [[nodiscard]] 应用在一些地方:operator new、std::async()、std::allocate()、std::launder() 和 std::empty()。此特性已通过 P0600R1 合并进了 C++20。

命名空间和枚举值上的属性

C++11 里属性的本意是可以将它们应用于所有可感知的地方:类、函数、变量、typedef、模版、枚举……但是遇到一些问题阻止了属性被用于命名空间和枚举值上。

C++17 里这个问题修复了。现在写下:

1
2
3
4
5
6
7
8
9
10
11
12
namespace [[deprecated("use BetterUtils")]] GoodUtils {
void DoStuff() { }
}

namespace BetterUtils {
void DoStuff() { }
}

int main()
{
GoodUtils::DoStuff();
}

时 Clang 会报出:

1
2
warning: 'GoodUtils' is deprecated: use BetterUtils
[-Wdeprecated-declarations]

另一个例子是把 deprecated 属性用于枚举值:

1
2
3
4
5
6
7
8
9
10
11
enum class ColorModes
{
RGB [[deprecated("use RGB8")]],
RGBA [[deprecated("use RGBA8")]],
RGBA16F,
RGB8,
RGBA8
};

// use:
auto colMode = ColorModes::RGBA;

GCC 里会得到:

1
2
warning: 'RGBA' is deprecated: use RGBA8
[-Wdeprecated-declarations]

扩展:本修改在 N4266(措辞) 和 N4196(讨论) 中描述。

忽略未知属性

此特性主要为了澄清。

C++17 之前如果你尝试使用某个编译器特定的属性,在另一个不支持它的编译器里编译时可能会得到一个编译错误。现在编译器会忽略这个属性声明,并且不会抛出任何错误(或者只是一个警告)。这点在之前的标准里并未提及,所以它需要澄清。

1
2
3
// 不支持 MyCompilerSpecificNamespace 的编译器会忽略此属性
[[MyCompilerSpecificNamespace::do_special_thing]]
void foo();

比如 GCC7.1 里会产生 warning:

1
2
3
warning: 'MyCompilerSpecificNamespace::do_special_thing'
scoped attribute directive ignored [-Wattributes]
void foo();

扩展:本修改在 P0283R2(措辞) 和 P0283R1(讨论) 中描述。

不必重复属性命名空间

此特性简化了用到多属性的用例,比如:

1
2
3
4
void f() {
[[rpr::kernel, rpr::target(cpu,gpu)]] // 重复
doTask();
}

提议的修改:

1
2
3
4
void f() {
[[using rpr: kernel, target(cpu,gpu)]]
doTask();
}

That simplification might help when building tools that automatically translate annotated code of that type into different programming models.

译注:没想到这是怎么样一个应用场景。

扩展:更多信息在:P0028R4

5.5 小结

C++17 里所有可用属性:

  • [[noreturn]]
  • [[carries_dependency]]
  • [[deprecated]]
  • [[deprecated(“reason”)]]
  • [[fallthrough]]
  • [[nodiscard]]
  • [[maybe_unused]]

编译器厂商可以声明自己的语法、属性列表和扩展。现代 C++ 里标准委员会试图提取属性的通用部分,目标是只坚持标准属性。但是这取决于你的平台。比如在嵌入式环境里可能会有很多必需的平台相关的注解以使代码和硬件粘在一起。

这里有一份非常重要的来自 Bjarne Stroustrup 关于 C++11 FAQ/属性的摘记

有理由担心属性将被用于创建语言方言。建议只使用属性来控制不影响程序含义但可能有助于检测错误(例如 [[noreturn]])或帮助优化器(例如 [[carries_dependency]])的事物。

5.6 编译器支持

略。

评论