Chapter7 新属性和属性特性

自从C++11起,就可以指明 属性(attributes) (允许或者禁用某些警告的注解)。 C++17引入了新的属性,还扩展了属性的使用场景,这样可以带来一些便利。

7.1 [[nodiscard]]属性

新属性[[nodiscard]]可以鼓励编译器在某个函数的返回值未被使用时给出警告 (这并不意味着编译器一定要给出警告)。

[[nodiscard]]通常应该用于防止某些因为返回值未被使用导致的不当行为。 这些不当行为可能是(译者注:请配合下边的例子理解这些不当行为):

  • 内存泄露 ,例如返回值中含有动态分配的内存,但并未使用。
  • 未知的或出乎意料的行为 ,例如因为没有使用返回值而导致了一些奇怪的行为。
  • 不必要的开销 ,例如因为返回值没被使用而进行了一些无意义的行为。

这里有一些该属性发挥所用的例子:

  • 申请资源但自身并不释放,而是将资源返回等待其他函数释放的函数应该被标记为 [[nodiscard]]。一个典型的例子是申请内存的函数, 例如malloc()函数或者分配器的allocate()成员函数。

然而,注意有些函数 可能 会返回一个无需再处理的值。例如,程序员可能会用0字节调用C函数 realloc()来释放内存,这种情况下的返回值无需之后调用free()函数释放。 因此,如果把realloc()函数标记为[[nodiscard]]将会适得其反。

  • 有时如果没有使用返回值将导致函数行为和预期不同,一个很好的例子是std::async() (C++11引入)。std::async()会在后台异步地执行一个任务并返回一个可以用来等待 任务执行结束的句柄(也可以通过它获取返回值或者异常)。然而,如果返回值没有被使用的话该调用 将变成同步的调用,因为在启动任务的语句结束之后未被使用的返回值的析构函数会立即执行,而析构 函数会阻塞等待任务运行结束。因此,不使用返回值导致的结果与std::async()的目的 完全矛盾。将std::async()标记为[[nodiscard]]可以让编译器给出警告。
  • 另一个例子是成员函数empty(),它的作用是检查一个对象(容器/字符串)是否 为空。程序员经常误用该函数来“清空”容器(删除所有元素):
    1. cont.empty();
    这种对empty()的误用并没有使用返回值,所以[[nodiscard]]可以检查出这种误用:
    1. class MyContainer {
    2. ...
    3. public:
    4. [[nodiscard]] bool empty() const noexcept;
    5. ...
    6. };
    这里的属性标记可以帮助检查这种逻辑错误。

如果因为某些原因你不想使用一个被标记为[[nodiscard]]的函数的返回值, 你可以把返回值转换为void

  1. (void)coll.empty(); // 禁止[[nodiscard]]警告

注意如果成员函数被覆盖或者隐藏时基类中标记的属性不会被继承:

  1. struct B {
  2. [[nodiscard]] int* foo();
  3. };
  4. struct D : B {
  5. int* foo();
  6. };
  7. B b;
  8. b.foo(); // 警告
  9. (void)b.foo(); // 没有警告
  10. D d;
  11. d.foo(); // 没有警告

因此你需要给派生类里相应的成员函数再次标记[[nodiscard]] (除非有某些原因导致你不想在派生类里确保返回值必须被使用)。

你可以把属性标记在函数前的所有修饰符之前,也可以标记在函数名之后:

  1. class C {
  2. ...
  3. [[nodiscard]] friend bool operator== (const C&, const C&);
  4. friend bool operator!= [[nodiscard]] (const C&, const C&);
  5. };

把属性放在friendbool之间或者booloperator== 之间是错误的。

尽管这个特性从C++17起引入,但它还没有在标准库中使用。因为这个提案出现的太晚了,所以最 需要它的std::async()也还没有使用它。不过这里讨论的所有例子,将在下一次C++标准 中实现(见C++20中通过的https://wg21.link/p0600r1提案)。

为了保证代码的可移植性,你应该使用[[nodiscard]]而不是一些不可移植的方案 (例如gcc和clang的 [[gnu:warn_unused_result]]或者Visual C++的_Check_return_)。

当定义new()运算符时, 你应该用[[nodiscard]]对该函数进行标记, 例如定义一个追踪所有new调用的头文件。

7.2 [[maybe_unused]]属性

新的属性[[maybe_unused]]可以避免编译器在某个变量未被使用时发出警告。 新的属性可以应用于类的声明、使用typedef或者using定义的类型、 一个变量、一个非静态数据成员、一个函数、一个枚举类型、一个枚举值等场景。

例如其中一个作用是定义一个可能不会使用的参数:

  1. void foo(int val, [[maybe_unused]] std::string msg)
  2. {
  3. #ifdef DEBUG
  4. log(msg);
  5. #endif
  6. ...
  7. }

另一个例子是定义一个可能不会使用的成员:

  1. class MyStruct {
  2. char c;
  3. int i;
  4. [[maybe_unused]] char makeLargerSize[100];
  5. ...
  6. };

注意你不能对一条语句应用[[maybe_unused]]。 因此,你不能直接用[[maybe_unused]]来抵消 [[nodiscard]]的作用:

  1. [[nodiscard]] void* foo();
  2. int main()
  3. {
  4. foo(); // 警告:返回值没有使用
  5. [[maybe_unused]] foo(); // 错误:maybe_unused不允许出现在此
  6. [[maybe_unused]] auto x = foo(); // OK
  7. }

7.3 [[fallthrough]]属性

新的属性[[fallthrough]]可以避免编译器在switch语句中某一个标签 缺少break语句时发出警告。例如:

  1. void commentPlace(int place)
  2. {
  3. switch (place) {
  4. case 1:
  5. std::cout << "very ";
  6. [[fallthrough]];
  7. case 2:
  8. std::cout << "well\n";
  9. break;
  10. default:
  11. std::cout << "OK\n";
  12. break;
  13. }
  14. }

这个例子中参数为1时将输出:

  1. very well

case 1case 2中的语句都会被执行。 注意这个属性必须被用作单独的语句,还要有分号结尾。 另外在switch语句的最后一个分支不能使用它。

7.4 通用的属性扩展

自从C++17起下列有关属性的通用特性变得可用:

  • 属性现在可以用来标记命名空间。例如,你可以像下面这样弃用一个命名空间:
    1. namespace [[deprecated]] DraftAPI {
    2. ...
    3. }
    这也可以应用于内联的和匿名的命名空间。
  • 属性现在可以标记枚举子(枚举类型的值)。 例如你可以像下面这样引入一个新的枚举值作为某个已有枚举值(并且现在已经被废弃)的替代:
    1. enum class City { Berlin = 0,
    2. NewYork = 1,
    3. Mumbai = 2,
    4. Bombay [[deprecated]] = Mumbai,
    5. ... };
    这里MumbaiBombay代表同一个城市的数字码,但使用Bombay 已经被标记为废弃的。注意对于枚举值,属性被放置在标识符 之后
  • 用户自定义的属性一般应该定义在自定义的命名空间中。现在可以使用using前缀 来避免为每一个属性重复输入命名空间。也就是说,如下代码:
    1. [[MyLib::WebService, MyLib::RestService, MyLib::doc("html")]] void foo();
    可以被替换为
    1. [[using MyLib: WebService, RestService, doc("html")]] void foo();
    注意在使用了using前缀时重复命名空间将导致错误:
    1. [[using MyLib: MyLib::doc("html")]] void foo(); // ERROR