8.2.3 预定义的通用属性

如上文所述,C++11预定义的通用属性包括[[noreturn]]和[[carries_dependency]]两种。

[[noreturn]]是用于标识不会返回的函数的。这里必须注意,不会返回和没有返回值的(void)函数的区别。没有返回值的void函数在调用完成后,调用者会接着执行函数后的代码;而不会返回的函数在被调用完成后,后续代码不会再被执行。

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

我们可以看看代码清单8-11所示的这个例子。

代码清单8-11


void DoSomething1();

void DoSomething2();

[[noreturn]]void ThrowAway(){

throw "expection";//控制流跳转到异常处理

}

void Func(){

DoSomething1();

ThrowAway();

DoSomething2();//该函数不可到达

}

//编译选项:clang++-std=c++11-c 8-2-3.cpp


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

不返回的函数除了是有异常抛出的函数外,还有可能是有终止应用程序语句的函数,或是有无限循环语句的函数等。事实上,在C++11的标准库中,我们都能看到形如:


[[noreturn]]void abort(void)noexcept;


这样的函数声明。这里声明的是最常见的abort函数。abort总是会导致程序运行的停止,甚至连自动变量的析构函数以及本该在atexit()时调用的函数全都不调用就直接退出了。因此声明为[[noreturn]]是有利于编译器优化的。

不过程序员还是应该小心使用[[noreturn]],也尽量不要对可能会有返回值的函数使用[[noreturn]]。代码清单8-12所示的是一个错误使用[[noreturn]]的例子。

代码清单8-12


include <iostream>

using namespace std;

[[noreturn]]void Func(int i){

//当参数i的值为0时,该函数行为不可估计

if(i<0)

throw "negative";

else if(i>0)

throw "positive";

}

int main(){

Func(0);

cout<<"Returned"<<endl;//无法执行该句

return 1;

}

//编译选项:clang++-std=c++11 8-2-4.cpp


代码清单8-12的例子中,Func调用后的打印语句永远不会被执行,因为Func被声明为[[noreturn]]。不过由于函数作者的疏忽,忘记了i==0时的状况,因此在i==0时,Func运行结束时还是会返回main的。在我们的实验机上,编译运行该例子会在运行时发生“段错误”。当然,具体的错误情况可能会根据编译器和运行时环境的不同而有所不同。不过总的来说,程序员必须审慎使用[[noreturn]]。

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

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


atomic<int*>a;

intp=(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)

}


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

而解决的方法正是使用[[carries_dependency]]。该通用属性既可以标识函数参数,又可以标识函数的返回值。当标识函数的参数时,它表示数据依赖随着参数传递进入函数,即不需要产生内存栅栏。而当标识函数的返回值时,它表示数据依赖随着返回值传递出函数,同样也不需要产生内存栅栏。更具体的我们可以看看代码清单8-13所示的例子。

代码清单8-13


include <iostream>

include <atomic>

using namespace std;

atomic<int*>p1;

atomic<int*>p2;

atomic<int*>p3;

atomic<int*>p4;

void func_in1(int*val){

cout<<*val<<endl;

}

void func_in2(int*[[carries_dependency]]val){

p2.store(val,memory_order_release);

cout<<*p2<<endl;

}

[[carries_dependency]]int*func_out(){

return(int*)p3.load(memory_order_consume);

}

void Thread(){

intp_ptr1=(int)p1.load(memory_order_consume);//L1

cout<<*p_ptr1<<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;

}

//编译选项:g++ -std=c++11 8-2-5.cpp-c


在代码清单8-13中,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]]还没有被编译器支持,而对一些强内存模型的平台来说,编译器也常常会忽略该通用属性,因此其可用性比较有限。不过与[[noreturn]]相同的是,[[carries_dependency]]只是帮助编译器进行优化,这符合通用属性设计的原则。当读者使用的平台是弱内存模型的时候,并且很关心并行程序的执行性能时,可以考虑使用[[carries_dependency]]。