什么时候我应该真正使用noexcept?

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

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

    不得不考虑是否需要在每一个函数声明之后附加noexcept将大大降低程序员的工作效率(坦率地说,这将是一件令人头疼的事情)。在哪些情况下,我应该更加小心地使用noexcept,在哪些情况下,我可以避免使用隐含的noexcept(false)?

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

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

171764 次浏览

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

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

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

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

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

一般来说,当您认为它实际上是有用的时,您应该使用noexcept。如果该类型的is_nothrow_constructible为真,则一些代码将采用不同的路径。如果您使用的代码会这样做,那么请随意使用noexcept适当的构造函数。

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

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

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

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

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

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

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

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

添加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吗?

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

最好考虑一个函数是否可能抛出异常作为函数的设计的一部分:与参数列表一样重要,以及一个方法是否是一个突变器(…# EYZ0)。声明“此函数从不抛出异常”是对实现的约束。省略它并不意味着该函数可能抛出异常;这意味着函数而且的当前版本,所有未来的版本都可能抛出异常。这是一个使实现更加困难的约束。但有些方法必须有实际用途的限制;最重要的是,可以从析构函数调用它们,也可以在提供强异常保证的方法中实现“回滚”代码。

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

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

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

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

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

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

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

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

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

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

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

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

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

有关不例外,堆栈展开和性能的相关问题详细介绍了需要栈展开时的开销。

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

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

其中终止是可接受的响应,未捕获的异常 将实现这一点,因为它将转换为对terminate()的调用 (§13.5.2.5)。同样,noexcept说明符(§13.5.1.1)也可以做到这一点 明确的渴望。< / p > 成功的容错系统是多级的。每个关卡都要处理 有尽可能多的错误,而不会太扭曲和离开 剩下的是更高的水平。异常支持这种观点。此外, terminate()通过提供转义来支持该视图 异常处理机制本身是损坏或如果它已经 未完全使用,从而留下未捕获的异常。同样的, noexcept为试图恢复的错误提供了一个简单的转义 似乎不可行。< / em > < / p >

double compute(double x) noexcept;     {
string s = "Courtney and Anya";
vector<double> tmp(10);
// ...
}
vector构造函数可能无法为它的10个double对象获取内存 并抛出std::bad_alloc。在这种情况下,程序终止。它 通过调用std::terminate()(§30.4.1.3)无条件地终止。 它不从调用函数中调用析构函数。它是 对象之间的作用域的析构函数是否由实现定义 thrownoexcept(例如,for s in compute())被调用。的 程序即将终止,所以我们不应该依赖任何 对象。通过添加noexcept说明符,我们指出我们的

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

#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&&)。在第二种情况下,它必须使用复制构造函数复制所有值。效率很低!