Effective Modern C++读书笔记:条款14

Aki 发布于 2023-01-11 261 次阅读


14只要函数不会发出异常,就为其加上noexcept声明:

首先,如果知道一个函数不可能抛出异常,则应该将其声明为noexcept,以提高代码的运行效率,但是如果函数却违法声明抛出了异常将导致程序直接中止。

noexcept可以让编译器生成更好的目标代码。因为在带有noexcept声明的函数中,优化器不需要在异常传出函数的前提下,将执行期栈保持可开解状态(栈展开:在运行时期间从函数调用栈中删除函数实体。如果异常没有在抛出它的函数中被处理,则会激活栈展开。),也不需要异常逸出函数的前提下,保证其中的对象以其被构造顺序的逆序完成析构。而以"throw()"异常规格声明的函数就享受不到noexcept带来的灵活优化。

而使用noexcept而非throw()好处有以下几个原因:

(1)声明为nocept,优化器会对函数做最大值的优化,包括将执行器栈保持在可开解状态。

(2)C++11新增了移动语义,所以STL标准库也做了一次大的性能调整,即在原先赋值操作地方,将采用"能移动则移动,必须复制才复制"策略。而能否移动的一个关键点就是STL函数是否声明了noexcept。而STL函数是否声明noexcept,取决于STL容器所含自定义对象对应的函数是否声明了noexcept。例如:如果自定义对象的移动拷贝函数声明了noexcept,则在对该对象进行push_back后扩张时,将使用移动拷贝而非复制拷贝,又或者对某对象的swap()成员函数声明了noexcept,则容器在调用STL的swap()函数时,将使用移动拷贝。

(3)默认的,所有内存释放函数(delete函数)或析构函数,无论是用户自定义还是编译器生成,都隐式的声明了noexcept。

下列是微软对于noexcept建议的使用:

某些类型的操作绝不会导致异常。 它们的实现应是可靠的,并且应该正常处理可能的错误情况。 它们不应使用异常来指示故障。 此规则标记此类操作未显式标记为 noexcept的情况,这意味着它们可能会引发异常,并且无法传达有关其可靠性的假设。

  • 特殊类型的操作:
    • 析构函数;
    • 默认构造函数;
    • 移动构造函数和移动赋值运算符;
    • 具有移动语义的标准函数: std::move 和 std::swap
struct S
{
    S() noexcept {}  //默认构造
    ~S() noexcept {}  //析构

    S(S&& s) noexcept {/*impl*/}   //移动构造
    S& operator=(S&& s) noexcept {/*impl*/}  //移动赋值

    S(const S& s) noexcept {/*impl*/}
   //拷贝构造
    S& operator=(const S& s) noexcept {/*impl*/}  //拷贝赋值
};

总结:

  • 只要函数不会发射异常,就为其加上noexcept声明【接口中的组成部分】,调用方可能会对其产生依赖
  • noexcept性质对于移动操作、swap、内存释放和析构函数最有价值
  • 带有noexcept可以获得更多优化的机会,进而提高运行效率
  • 影响可以调用代码的异常安全性