什么时候应该使用noexcept?

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

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

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

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

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

我认为现在提供一个“最佳实践”的答案为时太早,因为在实践中没有足够的时间来使用它。 如果在出现这个问题之后被问到这个问题,那么答案将会和现在非常不同。

不得不考虑在每个函数声明之后是否需要附加noexcept ,这将大大降低程序员的生产力(坦率地说,这将是一个痛苦)。

那么很明显,这个函数永远不会抛出。

什么时候可以切实地期望在使用noexcept之后观察性能改进? […]个人而言,我不在乎,因为为编译器提供了更多的自由度来安全地应用某些优化。

看起来最大的优化收益来自用户优化,而不是编译器的优化,因为可能检查noexcept和重载。 大多数编译器都遵循一个没有惩罚,如果你不抛出exception处理方法,所以我怀疑它会改变你的代码的机器代码级别(或任何东西),虽然也许通过删除处理来减less二进制大小码。

在大4(构造函数,赋值,而不是析构函数,因为它们已经是noexcept )中使用noexcept可能会导致最好的改进,因为noexcept检查在模板代码(如std容器)中是“常见的”。 例如, std::vector不会使用你的类的移动,除非它被标记为noexcept (否则编译器可以推导出它)。

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

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

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


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

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

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

select这些语义有两个原因:

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

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

几乎在编译器中的每一个优化都使用了一个叫做函数的“stream图”来推断什么是合法的。 stream程图由通常称为“块”的function(具有单个入口和单个出口的代码区域)以及块之间的边缘组成,以指示stream可以跳到哪里。 Noexcept改变stream程图。

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

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

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

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

在使用noexcept之后,除了观察性能改进之外,我可以切实noexcept吗? 具体来说,给出一个代码,在添加noexcept之后,C ++编译器能够生成更好的机器代码。

呃,从来没有? 永远不会是时间吗? 决不。

noexcept用于编译器性能优化的方式与const用于编译器性能优化的方式相同。 就是说,几乎从来没有。

noexcept主要用于允许“你”在编译时检测函数是否可以抛出exception。 记住:大多数编译器不会为exception发出特殊的代码,除非它实际上抛出一些东西。 所以noexcept不是给予编译器提示如何优化函数,而是提示如何使用函数。

move_if_noexcept这样的模板将检测移动构造函数是否使用noexcept定义,如果不是则返回一个const&而不是types的&& 。 如果这样做是非常安全的话,这是一种说法。

一般来说,当你认为这样做实际上是有用的时,你应该使用noexcept 。 如果该types的is_nothrow_constructible为true,则某些代码将采用不同的path。 如果您使用的代码会这样做,那么请随时不要使用合适的构造函数。

简而言之:将它用于移动构造函数和类似的构造,但是不要觉得你必须要坚持下去。

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

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

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

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

  1. 对于哪种情况,我应该更加小心使用noexcept,并且在哪些情况下我可以逃避隐含的noexcept(false)?

有四类function需要集中处理,因为它们可能会产生最大的影响:

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

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

这种优化在algorithm层面上,并不依赖于编译器优化。 它可以产生重大的影响,特别是如果元素复制昂贵。

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

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

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

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

我还推荐Scott Meyers的书“有效的现代C ++”,“项目14:声明函数,如果他们不会发出exception”,以便进一步阅读。

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

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

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

在Bjarne的话中:

终止是可接受的响应,未捕获的exception将实现,因为它变成了terminate()的调用(第13.5.2.5节)。 而且, noexcept说明符(§13.5.1.1)可以使这个愿望变得明确。

成功的容错系统是多级的。 每个级别都可以处理尽可能多的错误,而不会太扭曲,并将其余的提高到更高的级别。 例外情况支持该观点。 而且,如果exception处理机制本身被破坏,或者如果它被不完全使用, terminate()支持这个视图,从而使exception不被捕获。 同样, 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而写的。