C++11 鼓励对肯定不抛出异常的函数添加 noexcept 修饰

  1. 总览

    • 新机制

      • noexcept修饰的函数进行优化.

      • noexcept可以传导和查询.

    • 异常

      • 用异常代替if else.

      • if else统一到一个地方处理.

    • 异常

      • 必须处理的错误,直接抛出异常.

      • 不需要处理的错误.

    • noexcept函数

      • 任意时刻调用都不会出错.

      • noexcept可以是非noexcept组成.

    • 主流函数

      • 大部分都是可能抛出异常的.

      • 不一定非要追求noexcept,不要用return code代替异常.

    • 错误检测

      • 调用者保障.

      • 直接抛出异常.

  2. C++的异常

    • C++98异常

      • 可以通过throw(...)枚举可能抛出的异常. 如果抛出了预期外的则终止程序.
      • 但是统一性的问题. 使用者很可能会对使用的函数将要throw的异常比较关注.
    • C++98一致性

      • 使用程序的开发者对函数进行了修改,抛出的异常变化了.
      • 那么就需要改动throw(...)里面的枚举,那么就会导致一些列相关的代码也需要修改.
      • 带来的问题,编译器也不会提醒,就会带来一系列的问题.
      • 这种设计是不好的,但是有些场景又是必须的.
    • C++11

      • 只有两种状态: 肯定不会可能会 .

      • noexceptnoexcept(true) 修饰的肯定不会. 其他则可能会.

    • C++11C++98的差异

      • C++11noexcept修饰,即肯定不会抛异常的函数会进行优化.

      • C++98则会尝试优化,但是还是会保留一些额外的信息.

      • C++98不抛出异常:throw(),C++11则是noexcept,noexcept(true).

      • C++98出现期望之外的异常执行析构. C++11 遇到期望之外的会立即崩溃,不会清理资源.被优化掉了.

    • 接口设计

      • 在设计接口的时候就应该明确,是否可能会抛出异常.

      • 而且接口需要稳定,遵循开闭原则.

    • 函数是否异常的影响

      • 影响编译.

      • 影响使用者的代码实现和架构设计。

    • 重要性

      • 和函数的const修饰一样.

      • 但是noexcept不参与重载修饰.

    • C++98C++11差异案例

      • C++98会清理资源,返回到调用者,再崩溃.

      [root@localhost test]# g++ test.cpp -std=c++98
      [root@localhost test]# cat test.cpp
      #include <iostream>
      
      class T {
      public:
         T() {std::cout << __FUNCTION__ << std::endl;}
         ~T() {std::cout << __FUNCTION__ << std::endl;}
      };
      
      void show() throw()
      {
         T t;
         throw 1;
      }
      
      int main() {
         show();
      }
      [root@localhost test]# ./a.out
      T
      ~T
      terminate called after throwing an instance of 'int'
      Aborted
      
      
      • C++11不会,直接崩溃.栈回指只会发生在程序结束.编译器直接把释放的代码给优化掉了.不需要.因为已经保证肯定不会抛出异常.

      [root@localhost test]# g++ test.cpp -std=c++11
      [root@localhost test]# cat test.cpp
      #include <iostream>
      
      class T {
      public:
         T() {std::cout << __FUNCTION__ << std::endl;}
         ~T() {std::cout << __FUNCTION__ << std::endl;}
      };
      
      void show() noexcept
      {
         T t;
         throw 1;
      }
      
      int main() {
         show();
      }
      [root@localhost test]# ./a.out
      T
      terminate called after throwing an instance of 'int'
      Aborted
      
      
    • 编译器优化

      • void func() noexcept:全力优化; void func() throw():简单优化; void func():简单优化;

    • 小结

      • noexcept函数会被编译器优化这一点,开发者就应该对肯定不会抛出异常的函数加上这个修饰.

  3. noexcept场景分析

    • std::vector添加元素.

      • 添加导致,但是内存不足需要扩容.

    • 正确性问题

      • 如果拷贝的时候出现异常.数据如何处理.copy中发生异常,原来的数据还是完整的.

      • 拷贝失败,捕获还是什么?

      • C++11采用move的方式优化,但是问题是,move到一半,发生异常,原来的数据被修改,也无法回滚.

    • C++11做法

      • 拷贝根据类型的拷贝构造是否会抛出异常,选择使用元素的拷贝构造还是移动构造.

    • 案例

      #include <iostream>
      
      class T {
      public:
         T() {std::cout << this << std::endl;}
         T(const T& t) noexcept {std::cout << "copy"  << this << std::endl;}
         T(T&& t) noexcept {std::cout << "move"  << this << std::endl;}
         ~T() {std::cout << this << std::endl;}
      };
      
      void show(T t) noexcept(noexcept(T(std::move(t)))){
         if(noexcept(T(std::move(t)))) {
             std::cout << "noexcept t " << &t << std::endl;
             T d(std::move(t));
             std::cout << "noexcept d " << &d << std::endl;
         } else {
             std::cout << "except t " << &t << std::endl;
             T d(t);
             std::cout << "except d " << &d << std::endl;
         }
      }
      
      int main() {
         T t;
         show(t);
      }
      
      • noexcept版本的move.

      0x61fe1e
      copy 0x61fe1f
      noexcept t 0x61fe1f
      move 0x61fddf
      noexcept d 0x61fddf
      0x61fddf
      0x61fe1f
      0x61fe1e
      [Finished in 485ms]
      
      • except版本

      #include <iostream>
      
      class T {
      public:
         T() {std::cout << this << std::endl;}
         T(const T& t) noexcept {std::cout << "copy" << this << std::endl;}
         T(T&& t)  {std::cout << this << "move" << std::endl;}
         ~T() {std::cout << this << std::endl;}
      };
      
      void show(T t) noexcept(noexcept(T(std::move(t)))){
         if(noexcept(T(std::move(t)))) {
             std::cout << "noexcept t " << &t << std::endl;
             T d(std::move(t));
             std::cout << "noexcept d " << &d << std::endl;
         } else {
             std::cout << "except t " << &t << std::endl;
             T d(t);
             std::cout << "except d " << &d << std::endl;
         }
      }
      
      int main() {
         T t;
         show(t);
      }
      
      • 输出

      0x61fd3e
      copy 0x61fd3f
      except t 0x61fd3f
      copy 0x61fbae
      except d 0x61fbae
      0x61fbae
      0x61fd3f
      0x61fd3e
      [Finished in 472ms]
      
    • 建议

      • 拷贝,移动,swap这些函数实现成noexcept.

      • 拷贝和移动,析构默认是noexcept,但是默认生成的行为不符合预期.

    • 扩展

      #include <iostream>
      void show(int a) noexcept(noexcept(a) && noexcept(a)){
      }
      
      int main() {
         int a;
         show(a);
      }
      
      • 可以多个表达式,与或非.

  4. 优化和noexcept

    • 回顾

      • 前面的优化方案令人心动.是不是声明了程序就可以跑得更快了。

      • 但是正确性和开发效率,代码整洁等等都是需要考量的。

    • 设计

      • 先声明为noexcept,后期再改回来?

      • 随意改动会影响到其他使用到这个函数的人,不建议随意修改,自己使用另说.

    • 改成noexcept

      • 修改成noexcept,但是使用了可能抛异常的,就会导致异常无法传递,然后崩溃.

    • 主流

      • 都是 异常中立函数. 异常中立函数就是没有声明任何异常,也不是noexcept.

      • 这类可以传递使用的代码抛出的异常. 然后不做处理就直接放行.

    • 默认noexcept

      • 编译器提供的拷贝和移动.

    • 肯定不会抛异常的函数

      • 建议使用noexcept修饰.

    • 异常和返回值

      • 将异常捕获改成返回值的方式,然后把函数改成noexcept,这种就有点本末倒置了.
  5. noexcept和开发

    • noexcept

      • 虽然执行效率高,但是适用性不是特别广.

    • 异常的好处

      • 使用exception替代返回值的方式可以提高开发效率.

      • 可以减少因为返回值带来的if else判断分支.

      • 可以避免if else带来的函数膨胀.

      • 可以简化代码结构.将if else放到另外的地方处理.

    • 返回值的弊端

      • 虽然noexcept可能会很快,但是代价就是导致代码复杂.

      • 代码结构混乱,分支变多,维护麻烦.

      • 带来的一系列问题和开销可能还不如用异常来处理.

    • 提供功能

      • 功能提供者不应该检测数据的有效性.

      • 应该由调用者确保数据的合法性.

      • 外观模式的主要核心就是保障合法.

      • 出现问题则就应该直接抛出异常,让调用者自行处理.

  6. 函数设计

    • wide contracts | narrow contracts

      • wide就是不检测入参无限制,程序任意时刻可运行.

      • narrow就是入参有限制,错误参数程序会出错.

    • wide常见就是

      #include <iostream>
      void show(int a){
      }
      
      int main() {
         int a;
         show(a);
      }
      
      • 无任何要求,什么值都可以.

    • narrow

      #include <iostream>
      void show(int a){
         int c = 10 / a;
      }
      
      int main() {
         int a;
         show(a);
      }
      
      • a明显不能等于0. 但是不会检查,由调用者保证有效.

    • noexcept

      • 这类最可能将函数声明为noexcept.

    • narrow

      • 没有责任检测合法性, 会带来很多分支和性能的开销.

      • 应该由调用者检查合法性,有的时候甚至不需要检测.

      • 检查不强制,函数实现者也可以在函数内检测,不反对但是不推荐.

  7. noexcept不一定非要noexcept实现

    • wide

      • 什么场景都不会出现问题,直接noexcept.

    • widenarrow区分

      • wide一般用noexcept,narrow则没有.

    • noexcept调用非noexcept

      #include<iostream>
      void setup(); // functions defined elsewhere
      void cleanup();
      void doWork() noexcept
      {
      setup(); // set up work to be done
      // … // do the actual work
      cleanup(); // perform cleanup actions
      }
      
      int main() {
         doWork();
      }
      
      void setup() {
         printf("%s\n",__FUNCTION__);
      }
      
      void cleanup() {
         printf("%s\n",__FUNCTION__);
      }
      
      
      • 调用非noexcept,可能是C函数,没有noexcept.

      • 或者就是C++98的,没有noexcept.

  8. 总结

    • noexcept对使用者很重要.

    • noexcept的效率高,但是不要可以强求.

    • noexceptmove,swap析构这些场景非常适合.

    • 主流的是抛异常函数而非noexcept.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值