618

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

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

    必须考虑是否需要在每个noexcept函数声明后附加会大大降低程序员的工作效率(坦率地说,这会让人头疼)。在哪些情况下我应该更加小心使用,在哪些情况下我可以摆脱暗示?noexceptnoexcept(false)

  2. 使用后我什么时候可以真正期望观察到性能改进noexcept?特别是,给出一个代码示例,C++ 编译器在添加noexcept.

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

4

9 回答 9

213

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

必须考虑是否需要noexcept在每个函数声明后附加会大大降低程序员的工作效率(坦率地说,这会很痛苦)。

好吧,然后在很明显该函数永远不会抛出时使用它。

使用后我什么时候可以真正期望观察到性能改进noexcept?[...] 就我个人而言,我很在意,noexcept因为它为编译器提供了更大的自由度,可以安全地应用某些类型的优化。

似乎最大的优化收益来自用户优化,而不是编译器优化,因为有可能对其进行检查noexcept和重载。大多数编译器都遵循一个 no-penalty-if-you-don't-throw 异常处理方法,所以我怀疑它会在代码的机器代码级别上改变很多(或任何东西),尽管可能会通过删除来减少二进制大小处理代码。

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

于 2012-05-28T17:02:16.210 回答
159

正如我这些天不断重复的那样:语义第一

添加noexcept,noexcept(true)noexcept(false)首先是关于语义的。它只是偶然条件一些可能的优化。

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


好的,现在开始可能的优化。

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

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

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

  • noexcept即使依赖项尚未使用它也能立即受益(向后兼容性)
  • 允许指定noexcept何时调用理论上可能抛出的函数,但对于给定的参数预计不会抛出
于 2012-05-28T17:34:43.683 回答
97

这实际上确实对编译器中的优化器产生了(潜在的)巨大差异。多年来,编译器实际上已经通过函数定义后的空 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
}

这个函数的流程图是不同的 ifbar被标记noexcept(没有办法在 endbar和 catch 语句之间跳转)。当标记为noexcept时,编译器确定在 baz 函数期间 x 的值为 5 - x=5 块被称为“支配” baz(x) 块,没有从bar()到 catch 语句的边缘。

然后它可以做一些叫做“不断传播”的事情来生成更有效的代码。在这里,如果 baz 是内联的,则使用 x 的语句也可能包含常量,然后以前的运行时评估可以变成编译时评估,等等。

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

于 2012-05-28T18:33:41.060 回答
65

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

于 2012-09-24T07:31:45.237 回答
39

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

嗯,从来没有?从来没有时间?绝不。

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

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

像这样的模板move_if_noexcept将检测是否定义了移动构造函数,如果不是,noexcept则返回 aconst&而不是&&类型的 a。如果这样做非常安全,这是一种说法。

noexcept通常,当您认为这样做实际上有用时,您应该使用它。is_nothrow_constructible如果该类型为真,则某些代码将采用不同的路径。如果您正在使用可以做到这一点的代码,那么请随意使用noexcept适当的构造函数。

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

于 2012-05-28T16:59:26.607 回答
32

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

在终止是可接受的响应的情况下,未捕获的异常将实现这一点,因为它变成了对 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);
    // ...
}

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

于 2017-01-09T14:49:12.210 回答
28
  1. 我知道有许多函数示例永远不会抛出,但编译器无法自行确定。在所有这些情况下,我应该在函数声明中附加 noexcept 吗?

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

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

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

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

您应该专注于四类功能,因为它们可能会产生最大的影响:

  1. 移动操作(移动赋值运算符和移动构造函数)
  2. 交换操作
  3. 内存释放器(运算符删除,运算符删除 [])
  4. 析构函数(尽管这些是隐含的noexcept(true),除非你制作它们noexcept(false)

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

这种优化是算法层面的,不依赖编译器优化。它可能会产生重大影响,尤其是在复制元素成本高昂的情况下。

  1. 使用 noexcept 后,我​​什么时候可以真正期望观察到性能改进?特别是,给出一个 C++ 编译器在添加 noexcept 后能够生成更好的机器代码的代码示例。

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

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

关于noexcept、堆栈展开和性能的相关问题详细介绍了需要堆栈展开时的开销。

我还推荐 Scott Meyers 的书“Effective Modern C++”,“第 14 条:声明函数 noexcept 如果它们不会发出异常”以供进一步阅读。

于 2014-12-25T17:08:13.527 回答
20

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

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

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

于 2012-05-29T13:10:39.940 回答
1

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

#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()

如果我们删除移动构造函数中的 noexcept,这里是输出

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

主要区别是A(const A&&)vs A(const A&&)。在第二种情况下,它必须使用复制构造函数复制所有值。非常低效!

于 2021-04-17T03:00:08.287 回答