能使用noexcept就使用noexcept


  • noexcept优势

    1. 优化器方面

      在调用外部接口时通常最关心的是这个接口会不会抛出异常,下面是C++98和C++11不会抛出异常的写法:

       // C++98
       void Fun() throw();
       // C++11
       void Fun() noexcept;
      

      在运行期异常逸出函数Fun时,在C++98异常规格下调用栈会开解到Fun的调用方,程序中止,而在C++异常规格下程序中止前栈只是可能会开解,这里的区别在于开解调用栈和可能开解调用栈。

      在带有noexcept声明的函数中,优化器不需要在异常传出函数的前提下,将执行期栈保持在可开解状态,也不需要在异常逸出函数的前提下,保证所有其中的对象以被构造的顺序的逆序完成析构,而throw()异常规格声明的函数没有这样的优化

    2. 标准库方面

      在C++11面前,对以往的标准库优化就是使用移动语意,而受限于过往的实现有一些是强异常安全保证,例如:std::vector::push_back。基于这个事实,这些实现并不能暴力的使用移动语意,除非它知道移动操作不会抛出异常,标准库里的一些函数对于类似这样的优化都保持着“能移动则移动,必须拷贝则拷贝”的原则。而如何知道移动操作不会抛出异常就是借助noexcept。带来的收益就是提高了性能


  • 应用场景

    上述提出的优势很诱人,并不是所有场景都适用noexcept,下面是不适合应用noexcept场景:

    1. 在日常编写对外接口时如果使用了noexcept就意味着很长的一段时间接口不会抛出异常,可能随着需求的增加,需要去掉noexcept,这时就面临着需要调整客户端的代码的尴尬问题,这时就不太适合noexcept

    2. 异常中立的接口,接口本身不会抛出异常但是接口内的调用会抛出异常,实际使用过程中例如:对nlohmann::json接口的封装就是这样的。因为封装的这些接口可能会发射出“路过的异常”

    3. 在使用noexcept时,容易跳进“对外声明了noexcept,而接口返回状态码等”的陷阱,使得接口调用者增加很多逻辑分支,使得程序难维护,这时使用noexcept带来的收益远低于上述陷阱带来的问题

    有些接口具备了天然不会抛出异常的属性,这时为它们增加noexcept,例如游戏里计算装备提供暴击属性值等。对于这种不依赖外部输入的接口称为宽松契约wide contracts,对于宽松契约接口加上noexcept是理所当然的。还有一种依赖于外部输入的接口称为狭隘契约narrow contracts,例如这个接口void Fun(const std::string& strParam),假设依赖于参数strParam的长度必须小于32,Scott Meyer对这种狭隘契约接口建议是在调用接口前调用者有义务做好长度检查,个人觉得不管是调用者做长度检查还是接口做长度检查,这一步总是不可缺少的,如果调用者做了长度检查难道接口就不需要检查了吗?作为服务器工程师没有这样的勇气——接口内不做检查,因为在服务器运行时什么情况都有可能发生,所以个人觉得外部不要做长度检查,反而由接口做长度检查,反正总是要是检查的,这样做对于调用者减轻了负担。只是个人意见,也是迷惑的地方。当接口内部做长度检查时发现不满足前置条件,则可以抛出“未检查前置条件的异常”,所以狭隘契约接口不适合使用noexcept

    C++98使用内存释放函数可以抛出异常,例如delete、delete[],而C++11的内存释放函数和析构函数都隐式的使用noexcept。当类的数据成员(包含其他数据成员、继承来的)的析构函数显式的表达了会抛出异常的语意noexcept(false),则析构函数没有隐式的noexcept修饰。当然对析构函数显式的表达noexcept(false)是非常不推荐的,这也是C++11为这类函数增加隐式noexcept的原因