c++ - noexcept用法 - 我应该什么时候使用noexcept?




c++ noexcept用法 (6)

noexcept关键字可以适用于许多功能签名,但我不确定何时应该考虑在实践中使用它。 根据我目前阅读的内容, noexcept的最后一刻添加似乎解决了移动构造函数抛出时出现的一些重要问题。 但是,我仍然无法为一些实际问题提供令人满意的答案,这些问题让我首先阅读了更多关于noexcept的内容。

  1. 有很多函数的例子,我知道永远不会抛出,但编译器无法自行确定。 在所有这些情况下,我是否应该在函数声明中添加noexcept

    不得不考虑在每个函数声明之后是否需要附加noexcept ,这将大大降低程序员的生产力(坦率地说,这将是一个痛苦的屁股)。 对于哪种情况,我应该更加小心使用noexcept ,并且在这种情况下,我能否隐含noexcept(false)

  2. 什么时候可以切实地期望在使用noexcept之后观察性能改进? 具体来说,给出一个代码,在添加noexcept之后,C ++编译器能够生成更好的机器代码。

    就我个人而言,我关心的是noexcept因为为编译器提供了更多的自由度来安全地应用某些类型的优化。 现代编译器以这种方式利用noexcept吗? 如果不是,我希望他们中的一些人能够在不久的将来这样做吗?


  1. 有很多函数的例子,我知道永远不会抛出,但编译器无法自行确定。 在所有这些情况下,我是否应该在函数声明中添加否认?

noexcept是棘手的,因为它是函数接口的一部分。 特别是,如果你正在编写一个库,你的客户端代码可以依赖于noexcept属性。 稍后可能很难更改它,因为您可能会破坏现有的代码。 当您正在实现仅由您的应用程序使用的代码时,这可能不太重要。

如果你有一个不能抛出的函数,问问自己是否喜欢保持不noexcept或者会限制未来的实现? 例如,您可能希望通过抛出异常(例如,针对单元测试)引入非法参数的错误检查,或者您可能依赖可能更改其异常规范的其他库代码。 在这种情况下,保守而noexcept忽略是比较安全的。

另一方面,如果你确信函数不会抛出,并且它是规范的一部分是正确的,那么你应该声明它为noexcept 。 但是,请记住,如果您的实现更改,编译器将无法检测到noexcept违规行为。

  1. 对于哪种情况,我应该更加小心使用noexcept,并且在这种情况下,我能否隐含noexcept(false)?

有四类功能应该集中处理,因为它们可能会产生最大的影响:

  1. 移动操作(移动赋值操作符和移动构造函数)
  2. 交换操作
  3. 内存释放器(操作员删除,操作员删除[])
  4. 析构函数(虽然这些都是隐式的noexcept(true)除非你让他们noexcept(false)

这些函数通常应该是noexcept ,它很可能是库实现可以使用noexcept属性。 例如, std::vector可以使用非抛出移动操作而不牺牲强壮的异常保证。 否则,它将不得不回退到复制元素(就像它在C ++ 98中那样)。

这种优化在算法层面上,并且不依赖于编译器优化。 它可以产生重大影响,特别是如果这些元素的拷贝成本很高。

  1. 什么时候可以切实地期望在使用noexcept之后观察性能改进? 具体来说,给出一个代码,在添加noexcept之后,C ++编译器能够生成更好的机器代码。

noexcept没有任何异常规范或throw()的优点是,该标准允许编译器在堆栈展开时更加自由。 即使在throw()情况下,编译器也必须完全展开堆栈(并且必须按照对象构造的相反顺序)。

另一方面,在noexcept情况下,不需要这样做。 没有要求堆栈必须解开(但编译器仍然允许这样做)。 这种自由允许进一步的代码优化,因为它降低了始终能够展开堆栈的开销。

关于noexcept,堆栈展开和性能的相关问题在需要堆栈展开时会涉及更多关于开销的细节。

我还推荐Scott Meyers的书“有效的现代C ++”,“项目14:声明功能noexcept,如果他们不会发出异常”进一步阅读。


有很多函数的例子,我知道永远不会抛出,但编译器无法自行确定。 在所有这些情况下,我是否应该在函数声明中添加否认?

当你说“我知道[他们]永远不会抛出”时,你的意思是通过检查函数的实现你知道函数不会抛出。 我认为这种做法是彻头彻尾的。

最好考虑一个函数是否可以抛出异常成为函数设计的一部分:与参数列表一样重要,以及方法是否为增变器(... const )。 声明“这个函数从不抛出异常”是对实现的一个约束。 忽略它并不意味着该函数可能会抛出异常; 这意味着该函数的当前版本所有未来版本可能会引发异常。 这是一个制约因素,使得实施变得更加困难。 但是一些方法必须具有实际上有用的约束; 最重要的是,它们可以从析构函数中调用,但也可以在提供强大异常保证的方法中实现“回滚”代码。


noexcept可以显着提高某些操作的性能。 这不会发生在编译器生成机器代码的级别,而是通过选择最有效的算法:如其他人提到的那样,您可以使用函数std::move_if_noexcept来执行此选择。 例如, std::vector的增长(例如,当我们调用reserve )必须提供强大的异常安全保证。 如果它知道T的移动构造函数不抛出,它可以移动每个元素。 否则它必须复制所有T s。 这已经在这篇文章中详细描述。


在Bjarne的话中:

如果终止是可接受的响应,则未捕获的异常将实现此目的,因为它会变成terminate()(第13.5.2.5节)的调用。 另外, noexcept说明符(§13.5.1.1)可以使该愿望变得明确。

成功的容错系统是多级的。 每个级别都可以应对尽可能多的错误,而不会过度扭曲,并使其他级别更高。 例外情况支持该观点。 而且,如果异常处理机制本身被破坏或者它被不完全使用, terminate()通过提供转义来支持这个视图,从而使异常不被捕获。 同样, noexcept提供了一个简单的转义错误,试图恢复似乎不可行。

 double compute(double x) noexcept;   {       
     string s = "Courtney and Anya"; 
     vector<double> tmp(10);      
     // ...   
 }

vector构造函数可能无法为其10个双精度获取内存并抛出std::bad_alloc 。 在这种情况下,程序终止。 它通过调用std::terminate() (§30.4.1.3)无条件std::terminate() 。 它不调用调用函数的析构函数。 它是实现定义是否调用thrownoexcept之间的范围的析构函数(例如,在compute()中的s)。 程序即将终止,所以我们不应该依赖任何对象。 通过添加一个noexcept说明符,我们表明我们的代码不是为了处理throw而编写的。


正如我不断重复这些日子: 首先是语义

添加noexceptnoexcept(true)noexcept(false)首先是关于语义的。 它只是偶然地限制了一些可能的优化。

作为程序员的阅读代码, noexcept的存在类似于const :它可以帮助我更好地理解可能发生或可能不发生的事情。 因此,花一些时间考虑是否知道函数是否会抛出是值得的。 对于提醒,任何类型的动态内存分配可能会抛出。

好的,现在进行可能的优化。

最明显的优化实际上是在库中执行的。 如果可能的话,C ++ 11提供了许多可以知道函数是否为noexcept ,而标准库实现本身将使用这些特征来支持对他们操作的用户定义对象执行noexcept操作。 如移动语义

编译器可能只会从异常处理数据中删除一点(可能),因为它必须考虑到您可能撒谎的事实。 如果标记为noexcept的函数抛出,则调用std::terminate

选择这些语义有两个原因:

  • 立即从noexcept受益,即使依赖项已经不使用它(向后兼容)
  • 当调用理论上可能抛出但不期望给定参数的函数时,允许指定noexcept

这实际上确实对编译器中的优化器产生了(可能)巨大的差异。 通过函数定义之后的空throw()语句以及适当的扩展,编译器实际上已经有了这个功能多年。 我可以向你保证,现代编译器确实可以利用这些知识来生成更好的代码。

几乎在编译器中的每一个优化都使用了一个叫做流程图的函数来推断什么是合法的。 流程图包含通常称为功能块(具有单个入口和单个出口的代码区域)以及块之间的边界以指示流可以跳转到的位置。 Noexcept改变流程图。

你问了一个具体的例子。 考虑这个代码:

void foo(int x) {
    try {
        bar();
        x = 5;
        // other stuff which doesn't modify x, but might throw
    } catch(...) {
        // don't modify x
    }

    baz(x); // or other statement using x
}

如果bar被标记为noexcept,则该函数的流程图不同(在bar的结尾和catch语句之间没有办法执行跳转)。 当标记为noexcept时,编译器在baz函数期间确定x的值是5 - x = 5块被称为“控制”baz(x)块而没有从bar()到catch语句的边缘。 然后它可以做一些名为“不断传播”的东西来生成更高效的代码。 这里如果baz被内联,那么使用x的语句也可能包含常量,然后过去的运行时评估可以转化为编译时间评估等。

无论如何,简短的回答:noexcept让编译器生成更紧密的流程图,流程图用于推断各种常见的编译器优化。 对于编译器来说,这种性质的用户注释非常棒。 编译器会试图找出这些东西,但它通常不能(有问题的函数可能在另一个对象文件中,而编译器不可见或者传递地使用某些不可见的函数),或者当它存在时可能会抛出一个无关紧要的异常,因为它不会隐式地将其标记为noexcept(例如,分配内存可能会抛出bad_alloc)。





noexcept