C++20 标准属性


C++11后引入了 C++ 标准属性1。在 C++ 11 中,属性提供一种标准化的方法添加批注的其他信息,也可能不是特定于供应商的 C++ 构造 (包括但不是限于类、 函数、 变量和块)。 用于生成信息性消息,或应用特殊逻辑,编译特性化的代码时,编译器可以使用此信息。 编译器将忽略无法识别的任何属性,这意味着你不能定义自定义属性。使用此语法,属性将由双方括号标明。

下面是引自cppreference的C++标准属性列表2:

attribute-token属性标准作用
carries_dependency[[carries_dependency]]200809L(C++11)指示释放消费 std::memory_order 中的依赖链传进和传出该函数,这允许编译器跳过不必要的内存栅栏指令。
noreturn[[noreturn]]200809L(C++11)表示函数不返回值,并只能应用在函数上面,如果函数真的有返回值,那么该属性无效,并且会给出警告
deprecated[[deprecated]]
[[deprecated(“reason”)]]
201309L(C++14)表示某些实体已经废弃,或者因为某些原因废弃,可以用在类,定义类型名,变量,非静态数据成员,枚举,模板实例
fallthrough[[fallthrough]]201603L(C++17)指示从前一 case 标号直落是有意的,而在发生直落时给出警告的编译器不应该为此诊断。
maybe_unused[[maybe_unused]]201603L(C++17)压制编译器在未使用实体上的警告,若存在。
nodiscard[[nodiscard]]
[[nodiscard(“reason”)]]
201603L(C++17)
(C++20)
若返回值被舍弃,则鼓励编译器发布警告。
likely[[likely]]201803LC++20)指示编译器应该针对通过某语句的执行路径比任何其他执行路径更可能或更不可能的情况进行优化。
unlikely[[unlikely]]201803L(C++20)指示编译器应该针对通过某语句的执行路径比任何其他执行路径更可能或更不可能的情况进行优化。
no_unique_address[[no_unique_address]]201803L(C++20)指示非静态数据成员不需要拥有不同于其类的所有其他非静态数据成员的地址。

01 [[carries_dependency]]

A. cppreference.com中的解释

指示释放消费 std::memory_order 中的依赖链传进和传出该函数,这允许编译器跳过不必要的内存栅栏指令。

此属性可在两种情形中出现:

  1. 它可应用于函数或 lambda 表达式的形参声明,该情况下它指示从该形参的初始化向该对象的左值到右值转换中携带依赖。
  2. 它可应用于函数声明整体,该情况下它指示从返回值向函数调用表达式的求值中携带依赖。
    此属性必须出现在任意翻译单元中某个函数或其形参之一的首个声明上。若另一翻译单元中的该函数或其形参的首个声明上未使用该属性,则程序非良构;不要求诊断。

用法示例见 std::kill_dependency3

B. docs.microsoft.com中的解释

[[carries_dependency]] 指定该函数将传播数据依赖项排序方面线程同步。 该特性可以应用到一个或多个参数,以指定传入的参数传送到函数体的依赖项。 该特性可以应用于函数自身,以指定返回值包含函数的依赖项。 编译器可以使用此信息来生成更高效的代码。4

C.一篇简书blog上的解释

通用属性[[carries_dependency]] 则跟并行情况下的编译器优化有关。事实上,[[carries_depency]] 主要是为了解决弱内存模型平台上使用memory_order_consume内存顺序枚举问题。5

memory_order_consume的主要作用是保证对当前 “原子类型数据” 的读取操作先于所有之后关于该原子变量的操作完成,但它不影响其他原子操作的顺序。要保证这样的"先于发生" 的关系,编译器往往需要根据memory_model枚举值在原子操作间构建一系列的依赖关系,以减少在弱一致性模型的平台上产生内存栅栏。不过这样的关系则往往会由于函数的存在而被破坏。比如下面的代码:

tomic<int*> a;
...
int*p=(int*)a.load(memory_order_consume);
func(p);

上面的代码中,编译器在编译时可能并不知道func函数的具体实现,因此,如果要保证a.load先于任何关于a(或是p)的操作发生,编译器往往会在func函数之前加入一条内存栅栏。然而,如果func的实现是:

void func(int*p){
    //... 假设p2是一个atomic<int*>的变量
    p2.store(p,memory_order_release)
}

由于p2.store使用了memory_order_release的内存顺序,因此,p2.store对p的使用会被保证在任何关于p的使用之后完成。这样一来,编译器在func函数之前加入的内存栅栏就变得毫无意义,且影响了性能。同样的情况也会发生在函数返回的时候。

解决的方法就是使用[[carries_dependency]]。该通用属性既可以标识函数参数,又可以标识函数的返回值。

当标识函数的参数时,它表示数据依赖随着参数传递进入函数,即不需要产生内存栅栏。
而当标识函数的返回值时,它表示数据依赖随着返回值传递出函数,同样也不需要产生内存栅栏。

下面是相关的例子:

#include <iostream>
#include <atomic>
using namespace std;
atomic<int*> p1;
atomic<int*> p2;
atomic<int*> p3;
atomic<int*> p4;
//定义了4个原子类型
void func_in1(int*val){
     cout<<*val<<endl;
}

void func_in2(int*[[carries_dependency]] val){
     p2.store(val,memory_order_release);  //p2.store对p的使用会被保证在任何关于p的使用之后完成。
     cout<<*p2<<endl;
}

[[carries_dependency]] int*func_out(){
     return(int*)p3.load(memory_order_consume); //p3.load对p的使用会被保证在任何关于p的使用之前完成。
}

void Thread(){
     int* p_ptr1=(int*)p1.load(memory_order_consume); //L1
     
     cout<<*p_ptrl<<endl; //L2
     
     func_in1(p_ptr1); //L3
     
     func_in2(p_ptr1); //L4
     
     int*p_ptr2=func_out(); //L5
     
     p4.store(p_ptr2,memory_order_release); //L6
     
     cout<<*p_ptr2<<endl;
}

在代码中,L1句中,p1.load采用了memory_order_consume的内存顺序,因此任何关于p1或者p_ptr1的原子操作,必须发生在L1句之后。

这样一来,L2将由编译器保证其执行必须在L1之后(通过编译器正确的指令排序和内存栅栏)。

而当编译器在处理L3时,由于func_in1对于编译器而言并没有声明[[carries_dependency]]属性,编译器则可能采用保守的方法,在func_in1调用表达式之前插入内存栅栏。

而编译器在处理L4句时,由于函数func_in2使用了[[carries_dependency]], 编译器则会假设函数体内部会正确地处理内存顺序,因此不再产生内存栅栏指令。

事实上func_in2中也由于p2.store使用内存顺序memory_order_release, 因而不会产生任何的问题。

而当编译器处理L5句时,由于func_out的返回值使用了[[carries_dependency]],编译器也不会在返回前为p3.load(memory_order_consume) 插入内存栅栏指令去保证正确的内存顺序。

而在L6行中,我们看到p4.store使用了memory_order_release, 因此func_out不产生内存栅栏也是毫无问题的。

[[carries_dependency]] 只是帮助编译器进行优化,这符合通用属性设计的原则。 当使用的平台是弱内存模型(ARM 、 Itanium 、 Power PC)的时候,并且很关心并行程序的执行性能时,可以考虑使用 [[carries_dependency]]。

02 [[noreturn]]

指示函数不返回。

此属性仅应用到函数声明中正在声明的函数名。若拥有此属性的函数实际上返回,则行为未定义。

若函数的任何声明指定此属性,则其首个声明必须指定它。若函数在一个翻译单元中声明为带 [[noreturn]] 属性,而同一函数在另一翻译单元中声明为不带 [[noreturn]] 属性,则程序非良构;不要求诊断。

[[ noreturn ]] void f() {
  throw "error";
  // OK
}
 
void q [[ noreturn ]] (int i) {
  // 若以 <= 0 的参数调用则行为未定义
  if (i > 0) {
    throw "positive";
  }
}
 
// void h() [[noreturn]]; // 错误:属性应用到 h 的函数类型,而非 h 自身

C++的abort、exit、terminate等函数都具有[[noreturn]]属性。6

当使用[[noreturn]]属性标识函数后,调用该函数的后面的语句会得到编译器提示,warning C4702: 无法访问的代码。这将有助于发现遗漏未决问题。

[[noreturn]] 主要用于标识那些不会将控制流返回给原调用函数的函数,典型的例子有:有终止应用程序语句的函数、有无限循环语句的函数、有异常抛出的函数等。通过这个属性,开发人员可以告知编译器某些函数不会将控制流返回给调用函数,这能帮助编译器产生更好的警告信息,同时编译器也可以做更多的诸如死代码消除、免除为函数调用者保存一些特定寄存器等代码优化工作。
下面代码:

      void DoSomething1();
      void DoSomething2();
      [[noreturn]] void ThrowAway(){
           throw "expection"; //控制流跳转到异常处理
      }
      void Func(){
           DoSomething1();
           ThrowAway();
           DoSomething2(); // 该函数不可到达
      }

由于ThrowAway 抛出了异常,DoSomething2永远不会被执行。这个时候将ThrowAway标记为noreturn的话,编译器会不再为ThrowAway之后生成调用DoSomething2的代码。当然,编译器也可以选择为Func函数中的DoSomething2做出一些警告以提示程序员这里有不可到达的代码。7

03 [[deprecated]]

指示声明有[[deprecated]]/[[deprecated( 字符字面量 )]]属性的名字或实体被弃用,即允许但因故不鼓励使用。在弃用老版本的一些旧API接口时,经常会用到这个属性。
指示允许使用声明有此属性的名称或实体,但因故不鼓励使用。编译器通常会对其使用情况发出警告。若指定了 字符字面量,则它通常被包含于警告中。8
下列名字或实体的声明中允许使用这个属性:

  • class/struct/union:struct [[deprecated]] S;,
  • typedef 名,也包括别名声明:[[deprecated]] typedef S* PS;、using PS [[deprecated]] = S*;,
  • 变量,包括静态数据成员:[[deprecated]] int x;,
  • 非静态数据成员:union U { [[deprecated]] int n; };,
  • 函数:[[deprecated]] void f();,
  • 命名空间:namespace [[deprecated]] NS { int x; },
  • 枚举:enum [[deprecated]] E {};,
  • 枚举项:enum { A [[deprecated]], B [[deprecated]] = 42 };,
  • 模板特化:template<> struct [[deprecated]] X {};。
    声明时未弃用的名字可被重声明为 deprecated。声明为 deprecated 的名字不能通过重声明它而不带此属性变为未弃用。

04 [[fallthrough]]

指示从前一标号直落是有意的,而在发生直落时给出警告的编译器不应诊断它。
仅可应用到空语句以创建直落语句( fallthrough statement ): [[fallthrough]]; 。

直落语句仅可用于 switch 语句中,其中待执行的下个语句是该 switch 语句的带 case 或 default 标号的语句。若直落语句在循环中,则下个(带标号)语句必须是该循环的同一迭代的一部分。9

指示从前一标号直落是有意的,而在发生直落时给出警告的编译器不应诊断它。

void g() {}
void h() {}
void i() {}
void f(int n) {
    switch (n) {
    case 1:
    case 2:
        g();
        [[fallthrough]];
    case 3: // 直落时不警告
        h();
    case 4: // 编译器可在发生直落时警告
        if (n < 3) {
            i();
            [[fallthrough]]; // OK[warning C4468: fallthrough: 属性后面必须跟有用例标签或默认标签]
        }
        else {
            return;
        }
    case 5:
        while (false) {
            [[fallthrough]]; // 非良构:下一语句不是同一迭代的一部分
        }
    case 6:
        [[fallthrough]]; // 非良构:无后继的 case 或 default 标号
    }
}

05 [[maybe_unused]]

抑制针对未使用实体的警告。10
此属性可出现在下列实体的声明中:

  • class/struct/union:struct [[maybe_unused]] S;,
  • typedef,包括别名声明:[[maybe_unused]] typedef S* PS;,using PS [[maybe_unused]] = S*;,
  • 变量,包括静态数据成员:[[maybe_unused]] int x;,
  • 非静态数据成员:union U { [[maybe_unused]] int n; };,
  • 函数:[[maybe_unused]] void f();,
  • 枚举:enum [[maybe_unused]] E {};,
  • 枚举项:enum { A [[maybe_unused]], B [[maybe_unused]] = 42 };。
    若编译器针对未使用实体发布警告,则对于任何声明为 maybe_unused 的实体抑制该警告。
[[maybe_unused]] void f([[maybe_unused]] bool thing1,
                        [[maybe_unused]] bool thing2)
{
   [[maybe_unused]] bool b = thing1 && thing2;
   assert(b); // 发行模式中,assert 在编译中被去掉,因而未使用 b
              // 无警告,因为它被声明为 [[maybe_unused]]
} // 未使用参数 thing1 与 thing2,无警告

这个最长遇到的是函数参数未使用时,错误等级是/W4/Wall时,会报warning C4100 : “xxx”: 未引用的形参

void fun_maybe_unused2(int a) {
    //(void)a; // 老版本中为了不出warning C4100
}
void fun_maybe_unused([[maybe_unused]] int a) {} // 无警告

06 [[nodiscard]]

[[nodiscard]]11属性可以用于函数声明,枚举声明以及类声明中。如果你丢弃了一个声明为[[nodiscard]]的函数的返回值,编译器就会产生一个编译警告。同样的,如果你丢弃了函数中返回的(声明为)[[nodiscard]]枚举或者(声明为)[[nodiscard]]类,编译器同样会给出警告。抑制该类警告的一种方法就是对返回值进行一次void转型操作。

struct [[nodiscard]] error_info{ };
error_info enable_missile_safety_mode() {
    return error_info{};
};
void launch_missiles() {};
void test_missiles() {
    enable_missile_safety_mode(); // 编译器可在舍弃 nodiscard 值时发布警告
    (void)enable_missile_safety_mode(); // 无编译警告
    error_info ei = enable_missile_safety_mode(); // 无编译警告
    launch_missiles();
}
error_info& foo(error_info& x) { return x; };
void f1() {
    error_info ei;
    foo(ei); // 并非按值返回 nodiscard 类型,无警告
}

07 [[likely]]/[[unlikely]]

允许编译器为包含该语句的执行路径,比任何其他不包含该语句的执行路径,更可能或更不可能的情况进行优化。12

这些属性可应用于标号或语句(除了声明语句)。它们不可同时应用到同一标号或语句。

  1. 应用到语句,允许编译器为包含该语句的执行路径,比任何其他不包含该语句的执行路径更可能的情况进行优化。
  2. 应用到语句,允许编译器为包含该语句的执行路径,比任何其他不包含该语句的执行路径更不可能的情况进行优化。
    当且仅当执行路径中含有到某个标号的跳转时,才认为该执行路径包含该标号:
int flikely(int i) {
    switch (i) {
    case 1: [[fallthrough]];
    [[likely]] case 2: return 1;
    }
    return 2;
}

i == 2被认为比 i 的任何其他值更可能, 但 [[likely]]i == 1的情况无效果,尽管它直落到 case 2: 标号。

08 [[no_unique_address]]

[[no_unique_address]]指示此数据成员不需要具有不同于其类的所有其他非静态数据成员的地址。13

应用到非位域非静态数据成员的声明中所声明的名字。
指示此数据成员不需要具有不同于其类的所有其他非静态数据成员的地址。这表示若该成员拥有空类型(例如无状态分配器),则编译器可将它优化为不占空间,正如同假如它是空基类一样。若该成员非空,则其中的任何尾随填充空间亦可复用于存储其他数据成员。

struct Empty {}; // 空类

struct X {
    int i;
    Empty e;
};

struct Y {
    int i;
    [[no_unique_address]] Empty e;
};

struct Z {
    char c;
    [[no_unique_address]] Empty e1, e2;
};

struct W {
    char c[2];
    [[no_unique_address]] Empty e1, e2;
};

void f_no_unique_address() {
    // 任何空类类型对象的大小至少为 1
    static_assert(sizeof(Empty) >= 1);

    // 至少需要多一个字节以给 e 唯一地址
    static_assert(sizeof(X) >= sizeof(int) + 1);

    // 优化掉空成员
    std::cout << "sizeof(Y) == sizeof(int) is " << std::boolalpha
        << (sizeof(Y) == sizeof(int)) << '\n';

    // e1 与 e2 不能共享同一地址,因为它们拥有相同类型,尽管它们标记有 [[no_unique_address]]。
    // 然而,其中一者可以与 c 共享地址。
    static_assert(sizeof(Z) >= 2);

    // e1 与 e2 不能拥有同一地址,但它们之一能与 c[0] 共享,而另一者与 c[1] 共享
    std::cout << "sizeof(W) == 2 is " << (sizeof(W) == 2) << '\n';

    // 可能输出结果:
    //sizeof(Y) == sizeof(int) is true
    //sizeof(W) == 2 is true
}

  1. C++ 标准属性 ↩︎

  2. C++标准属性说明符序列 ↩︎

  3. carries_dependency ↩︎

  4. C++ 标准属性 ↩︎

  5. 深入理解C++11:C++11新特性解析与应用之 融入实际应用 ↩︎

  6. noreturn ↩︎

  7. 深入理解C++11:C++11新特性解析与应用之 融入实际应用 ↩︎

  8. deprecated ↩︎

  9. fallthrough ↩︎

  10. maybe_unused ↩︎

  11. nodiscard ↩︎

  12. likely/unlikely ↩︎

  13. no_unique_address ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值