深入使用noexcept
深入使用noexcept
简介
noexcept
是C++11引入的,表明函数是否会抛出异常。正确使用它可以优化性能。
noexcept
使用语法有两种:
- noexcpet
- noexcept(expression)
第二种使用方式允许用表达式决定是否noexcept
起效果,当expression
的值为true的时候起效果,否则不起效。expression
是编译时求值,一切都是在编译时决定。
好处
如果一个函数标注成noexcept
,
- 可以选择移动构造函数 / 移动赋值运算符;
- 编译器就不用生成异常处理代码了,因此可以优化编译。
坏处
如果noexcept的函数执行时出了异常,包括所调用的函数抛出的异常,程序会马上terminate,即使套上try...catch
也仍旧会terminate。并且编译器不会帮你检查这样的风险。
适用场景
在需要决定是调用移动构造函数(或者移动赋值运算符)还是复制构造函数(或者复制赋值运算符)时,noexcept
会影响决定。因为移动语法会“破坏”原来的源对象的内容,造成无法在出现异常情况下恢复状态。因此只有移动构造函数(或者移动赋值运算符)标明为noexcept
时,才能在这种情况下使用移动构造函数(或者移动赋值运算符)替代复制构造函数(或者复制赋值运算符)。
一个例子就是STL库中的vector
的扩容,扩容涉及到是复制对象还是移动对象的问题,就是上述的问题。当对象的移动构造函数可能会抛出异常的时候,vector
是”不敢“在这个场景下调用的,因为出了异常无法原恢复状态。
详细逻辑如下:
- 当使用复制构造时,抛出异常时只要把已复制的对象销毁,新分配的内存释放,一切还能恢复到跟以前一样;
- 当使用移动构造时,容器中的原来的元素的状态已经被移动构造破坏了,无法恢复到跟以前一样。
以下是测验代码:
class A {
public:
A() { std::cout << "constructor" << std::endl; }
A(const A& a) { std::cout << "copy constructor" << std::endl; }
A(const A&& a) noexcept { std::cout << "move constructor" << std::endl; } // 有noconcept时,扩容时用移动构造
// A(const A&& a) { std::cout << "move constructor" << std::endl; } // 去掉noconcept时,扩容时用拷贝构造
};
int main() {
std::vector<A> v;
v.reserve(1);
for (int i = 0; i < 10; i++) {
A a; // 构造一个A类的实例。
v.push_back(a); // 添加进容器时会调用一次复制构造,如果容量不够则会扩容,这时候会选择复制构造还是移动构造。
}
return 0;
}
不适用场景
其他情况均不太适合使用。因为:
- 编译器不会帮你做检查,假如一个标注noexcept的函数调用未标注noexcept的函数,是可以顺利编译的。但是未标注nonexcept的函数是不保证不抛异常的。
- 一个函数加上
noexcept
之后就可能很难移除,因为其他代码可能会直接或间接引用到它,且假设不会有异常; - 如果一个标注了
noexcept
的函数自己或者调用的函数(直接或间接)抛出异常,直接终止,非常简单粗暴。
因此出了几个有限的适用场景外,其他情况下不要用noexcept
。
实验结果
以下图表是来自于 C++ noexcept and move constructors effect on performance in STL Containers — TRYING TO FIND THE OBVIOUS (hlsl.co.uk) 这篇博客的实验结果。
实验结果根据实验结果,性能提升了两倍!
总结
虽然noexcept
会在某些情况下提升性能,但是由于它的危险性,包括发生异常直接终止程序且编译器不会帮你检查,除了以下情况下都不建议使用。
- 移动构造函数
- 移动赋值运算符
- 析构函数
- 简单函数
1、2 已经在前面说过了,不再赘述。对于3、4,析构函数本身不应该抛异常,简单函数一般不会发生异常,因此可以放心标注noexcept
。