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

There are many examples of functions that I know will never throw, but for which the compiler cannot determine so on its own. Should I append noexcept to the function declaration in all such cases? Having to think about whether or not I need to append noexcept after every function declaration would greatly reduce programmer productivity (and frankly, would be a pain in the neck). For which situations should I be more careful about the use of noexcept, and for which situations can I get away with the implied noexcept(false)? When can I realistically expect to observe a performance improvement after using noexcept? In particular, give an example of code for which a C++ compiler is able to generate better machine code after the addition of noexcept. Personally, I care about noexcept because of the increased freedom provided to the compiler to safely apply certain kinds of optimizations. Do modern compilers take advantage of noexcept in this way? If not, can I expect some of them to do so in the near future?


在使用noexcept后,什么时候我可以实际地观察到性能的改善?特别地,请给出一个c++编译器在添加noexcept后能够生成更好的机器代码的代码示例。

嗯,永远退不了?从来没有时间?从来没有。

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

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

像move_if_noexcept这样的模板将检测移动构造函数是否定义了noexcept,如果没有,则返回一个const&而不是该类型的&&。这是一种表示在非常安全的情况下移动的方式。

一般来说,你应该使用nono,除非你认为这样做确实有用。如果该类型的is_nothrow_constructible为真,则某些代码将采用不同的路径。如果你使用的代码会这样做,那么可以随意使用noexcept适当的构造函数。

简而言之:将它用于move构造函数和类似的构造,但不要觉得你必须用它发疯。


我认为现在给出一个“最佳实践”的答案还为时过早,因为还没有足够的时间在实践中使用它。如果在抛出说明符刚出来的时候被问到这个问题,那么答案将与现在大不相同。

不得不考虑是否需要在每个函数声明后添加noexcept将大大降低程序员的工作效率(坦率地说,这将是一种痛苦)。

那么,当函数明显不会抛出时使用它。

在使用noexcept之后,我什么时候才能真正看到性能的改善?[…就我个人而言,我不关心什么,因为给编译器提供了更多的自由,可以安全地应用某些类型的优化。

似乎最大的优化收益来自用户优化,而不是编译器的优化,因为可以检查noexcept和重载它。大多数编译器都遵循“如果不抛出就不惩罚”的异常处理方法,因此我怀疑它会在代码的机器代码级别上改变太多(或任何东西),尽管可能会通过删除处理代码来减小二进制大小。

在四大函数中使用noexcept(构造函数,赋值函数,而不是析构函数,因为它们已经是noexcept了)可能会带来最好的改进,因为noexcept检查在模板代码中是“常见的”,比如在std容器中。例如,std::vector不会使用类的move,除非它标记为noexcept(或者编译器可以以其他方式推断它)。


正如我这些天一直在重复的:语义第一。

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

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


现在来看可能的优化。

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

编译器可能只从异常处理数据中删减了一些内容,因为它必须考虑到您可能说谎的事实。如果标记为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的语句也可能包含常量,然后过去的运行时求值可以转换为编译时求值,等等。

Anyway, the short answer: noexcept lets the compiler generate a tighter flow graph, and the flow graph is used to reason about all sorts of common compiler optimizations. To a compiler, user annotations of this nature are awesome. The compiler will try to figure this stuff out, but it usually can't (the function in question might be in another object file not visible to the compiler or transitively use some function which is not visible), or when it does, there is some trivial exception which might be thrown that you're not even aware of, so it can't implicitly label it as noexcept (allocating memory might throw bad_alloc, for example).


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

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

It is better to consider whether a function may throw exceptions to be part of the design of the function: as important as the argument list and whether a method is a mutator (... const). Declaring that "this function never throws exceptions" is a constraint on the implementation. Omitting it does not mean the function might throw exceptions; it means that the current version of the function and all future versions may throw exceptions. It is a constraint that makes the implementation harder. But some methods must have the constraint to be practically useful; most importantly, so they can be called from destructors, but also for implementation of "roll-back" code in methods that provide the strong exception guarantee.


noexcept can dramatically improve performance of some operations. This does not happen at the level of generating machine code by the compiler, but by selecting the most effective algorithm: as others mentioned, you do this selection using function std::move_if_noexcept. For instance, the growth of std::vector (e.g., when we call reserve) must provide a strong exception-safety guarantee. If it knows that T's move constructor doesn't throw, it can just move every element. Otherwise it must copy all Ts. This has been described in detail in this post.


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

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

如果你有一个不能抛出的函数,问问自己它是否会像stay noexcept一样,还是会限制将来的实现?例如,您可能希望通过抛出异常来引入对非法参数的错误检查(例如,对于单元测试),或者您可能依赖于其他可能改变其异常规范的库代码。在这种情况下,保守起见,不省略例外。

另一方面,如果你确信函数永远不会抛出,并且它是规范的一部分是正确的,你应该声明它为noexcept。但是,请记住,除非你的实现发生了变化,否则编译器将无法检测到违反no_type的情况。

在哪些情况下我应该更加小心地使用noexcept,在哪些情况下我可以使用隐含的noexcept(false)?

这里有四类你应该关注的函数,因为它们可能会产生最大的影响:

移动操作(移动赋值操作符和移动构造函数) 互换操作 内存释放器(operator delete, operator delete[]) 析构函数(尽管它们是隐式的noexcept(true),除非你将它们设置为noexcept(false))

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

这种优化是在算法级别上的,不依赖于编译器优化。它可能会产生重大影响,特别是如果复制这些元素的成本很高的话。

在使用noexcept之后,我什么时候才能真正看到性能的改善?特别地,请给出一个c++编译器在添加noexcept后能够生成更好的机器代码的代码示例。

noexcept相对于无异常规范或throw()的优点是,当涉及到堆栈展开时,标准允许编译器有更多的自由。即使在throw()情况下,编译器也必须完全展开堆栈(并且必须以与对象结构完全相反的顺序进行)。

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

有关noexcept、堆栈展开和性能的相关问题更详细地讨论了需要堆栈展开时的开销。

我还推荐Scott Meyers的书“Effective Modern c++”,“Item 14:如果函数不会触发异常,则声明函数noexcept”,以供进一步阅读。


用Bjarne的话来说(c++编程语言,第4版,第366页):

Where termination is an acceptable response, an uncaught exception will achieve that because it turns into a call of terminate() (§13.5.2.5). Also, a noexcept specifier (§13.5.1.1) can make that desire explicit. Successful fault-tolerant systems are multilevel. Each level copes with as many errors as it can without getting too contorted and leaves the rest to higher levels. Exceptions support that view. Furthermore, terminate() supports this view by providing an escape if the exception-handling mechanism itself is corrupted or if it has been incompletely used, thus leaving exceptions uncaught. Similarly, noexcept provides a simple escape for errors where trying to recover seems infeasible. double compute(double x) noexcept; { string s = "Courtney and Anya"; vector<double> tmp(10); // ... } The vector constructor may fail to acquire memory for its ten doubles and throw a std::bad_alloc. In that case, the program terminates. It terminates unconditionally by invoking std::terminate() (§30.4.1.3). It does not invoke destructors from calling functions. It is implementation-defined whether destructors from scopes between the throw and the noexcept (e.g., for s in compute()) are invoked. The program is just about to terminate, so we should not depend on any object anyway. By adding a noexcept specifier, we indicate that our code was not written to cope with a throw.


这里有一个简单的例子来说明什么时候它真的很重要。

#include <iostream>
#include <vector>
using namespace std;
class A{
 public:
  A(int){cout << "A(int)" << endl;}
  A(const A&){cout << "A(const A&)" << endl;}
  A(const A&&) noexcept {cout << "A(const A&&)" << endl;}
  ~A(){cout << "~S()" << endl;}
};
int main() {
  vector<A> a;
  cout << a.capacity() << endl;
  a.emplace_back(1);
  cout << a.capacity() << endl;
  a.emplace_back(2);
  cout << a.capacity() << endl;
  return 0;
}

这是输出

0
A(int)
1
A(int)
A(const A&&)
~S()
2
~S()
~S()

如果我们在move构造函数中删除noexcept,输出如下

0
A(int)
1
A(int)
A(const A&)
~S()
2
~S()
~S()

关键的区别是A(const A&&) vs A(const A&&)。在第二种情况下,它必须使用复制构造函数复制所有值。效率很低!