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


507

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

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

    必须考虑noexcept每个函数声明之后是否都需要追加操作,这将大大降低程序员的工作效率(坦率地说,这将是一件痛苦的事情)。在哪些情况下我应该更小心使用它noexcept,在哪些情况下我可以摆脱隐含的含义noexcept(false)

  2. 使用后,我什么时候才能实际观察到性能改善noexcept?特别是,给出一个示例代码,在添加之后C ++编译器能够为其生成更好的机器代码noexcept

    我个人很在乎,noexcept因为为编译器提供了增加的自由度,可以安全地应用某些优化。现代编译器是否noexcept以此方式利用?如果没有,我可以指望其中的一些在不久的将来这样做吗?


40
move_if_nothrow如果有noexcept move ctor ,则使用(或whatchamacallit)的代码将获得性能改进。
R. Martinho Fernandes


5
move_if_noexcept
Nikos

Answers:


180

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

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

好吧,然后在明显无法使用该函数的情况下使用它。

使用后,我什么时候才能实际观察到性能改善noexcept?我个人很在乎,noexcept因为为编译器提供了增加的自由度,可以安全地应用某些优化。

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

使用noexcept在四大(构造函数,赋值,析构函数不是因为他们已经noexcept)很可能会造成最好的改进如noexcept支票模板代码中“普通”,如std容器。例如,std::vector除非被标记noexcept(否则编译器可以推断出它),否则将不会使用您的类的移动。


6
我认为这个std::terminate技巧仍然遵循零成本模型。也就是说,恰好发生的是,noexcept函数中的指令范围被映射为调用std::terminateif throw,而不是使用堆栈展开器。因此,我怀疑它比常规的异常跟踪有更多的开销。
Matthieu M.

4
@Klaim请参阅:stackoverflow.com/a/10128180/964135实际上,它必须是非抛出的,但是可以noexcept保证。
Pubby

3
“如果调用了一个noexcept函数,则将std::terminate调用它,这似乎将涉及少量的开销”
Potatoswatter 2012年

8
@Pubby C ++异常处理通常无需任何开销即可完成,除了跳转表外,跳转表将可能抛出的调用站点地址映射到处理程序入口点。删除这些表与完全删除异常处理非常接近。唯一的区别是可执行文件的大小。可能一文不值。
Potatoswatter 2012年

26
“那么当该函数永远不会抛出时,请使用它。” 我不同意。noexcept是函数界面的一部分;您不应该仅仅因为您当前的实现不抛出而添加它。我不确定该问题的答案是否正确,但是我非常有信心,今天的功能如何发生与之无关……
Nemo 2014年

133

正如我最近一直在重复的那样:语义优先

首先添加noexceptnoexcept(true)并且noexcept(false)最重要的是语义。它只是附带条件了许多可能的优化。

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


好的,现在继续可能的优化。

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

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

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

  • noexcept即使依赖项尚未使用它,也会立即受益(向后兼容)
  • 允许规范noexcept何时调用可能在理论上抛出但对于给定参数不期望的函数

2
也许我很天真,但是我可以想象一个仅调用noexcept函数的函数不需要做任何特殊的事情,因为可能出现的任何异常都会terminate在到达此级别之前触发。这与必须处理和传播bad_alloc异常有很大的不同。

8
是的,可以按照您建议的方式定义noexcept,但这将是一个真正无法使用的功能。如果某些条件不成立,那么许多函数都会抛出该异常,即使您知道满足这些条件,也无法调用它们。例如,任何可能抛出std :: invalid_argument的函数。
tr3w 2013年

3
@MatthieuM。回复有点晚,但是。标有noexcept的函数可以调用可以抛出的其他函数,保证该函数不会发出异常,即它们只需要自己处理该异常即可!
Honf 2013年

4
很久以前,我对这个答案进行了投票,但是经过阅读和思考之后,我有一个评论/问题。“移动语义”是我见过的唯一一个例子,该例子noexcept显然对您有所帮助/一个好主意。我开始认为移动构造,移动分配和交换是仅有的几种情况...您还知道其他吗?
Nemo 2014年

5
@Nemo:在Standard库中,它可能是唯一的库,但是它展现了可以在其他地方重用的原理。移动操作是一种将某些状态暂时置于“ limbo”状态的操作,只有在状态为“ noexcept有人”时,有人才能自信地将其用于随后可以访问的一条数据上。我可以看到这个想法在其他地方使用,但是Standard库在C ++中非常薄,它仅用于优化我认为的元素副本。
Matthieu M. 2014年

77

实际上,这确实对编译器中的优化程序产生了(可能)巨大的影响。多年来,编译器实际上已经通过在函数定义以及适当的扩展之后使用空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(没有执行方法可以在end bar和catch语句之间跳转)。当标记为时noexcept,编译器确定在baz函数期间x的值为5-据说x = 5块“支配”了baz(x)块,而没有bar()catch语句的边缘。

然后,它可以执行称为“恒定传播”的操作以生成更有效的代码。在这里,如果内联baz,则使用x的语句也可能包含常量,然后可以将以前是运行时评估的内容转换为编译时评估等。

总之,答案很简单:noexcept让编译器生成更紧密的流程图,该流程图用于推理各种常见的编译器优化。对于编译器来说,这种性质的用户注释很棒。编译器会尝试找出这些东西,但是通常不会(问题函数可能在另一个目标文件中,编译器不可见,或者可以过渡地使用某些不可见的函数),或者当它出现时,您可能根本没有意识到的一些琐碎的异常,因此它无法隐式地将其标记为noexcept(例如,分配内存可能会抛出bad_alloc)。


3
这实际上在实践中有所作为吗?该示例是人为设计的,因为之前没有任何东西x = 5可以抛出。如果该部分的内容有try任何用途,则推理将不成立。
Potatoswatter 2012年

7
我要说的是,它确实在优化包含try / catch块的函数方面确实发挥了作用。我给出的示例虽然人为设计,但并不详尽。更大的一点是,noexcept(像之前的throw()语句一样)有助于编译生成较小的流程图(更少的边缘,更少的块),这是它随后进行的许多优化的基本组成部分。
特里·马哈菲

编译器如何识别代码可以引发异常?数组的访问是否被视为可能的例外?
Tomas Kubes 2015年

3
@ qub1n如果编译器可以看到函数的主体,则可以查找显式throw语句,否则new可能会抛出类似的问题。如果编译器看不到主体,则它必须依赖的存在或不存在noexcept。普通的数组访问通常不会生成异常(C ++没有边界检查),因此不会,数组访问不会单独导致编译器认为函数会引发异常。(出站访问是UB,不是保证的例外。)
cdhowie

@cdhowie“ 它必须依靠 noexcept 的存在或不存在 ”或throw()pre-noexcept C ++的存在
curiousguy

57

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


4
附录:这意味着如果您定义了移动构造函数或向其添加noexcept了移动赋值运算符(如果适用)!隐式定义的移动成员函数已noexcept自动添加到它们中(如果适用)。
mucaho 2015年

33

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

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

noexcept用于编译器性能优化,其方式与const用于编译器性能优化的方式相同。也就是说,几乎永远不会。

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

像这样的模板move_if_noexcept将检测move构造函数是否使用定义,如果不是,noexcept则将返回a const&而不是&&类型的a 。如果这样做很安全,那是一种动议的方式。

通常,您应该noexcept在认为这样做确实有用的情况下使用。如果is_nothrow_constructible该类型为true,则某些代码将采用不同的路径。如果您使用的代码可以做到这一点,那么noexcept适合适当的构造函数。

简而言之:将其用于move构造函数和类似的构造,但不必觉得必须为此而烦恼。


13
严格来说,move_if_noexcept不会返回副本,而是会返回const lvalue-reference而不是rvalue-reference。通常,这将导致呼叫者进行复制而不是移动,但move_if_noexcept不会进行复制。否则,很好的解释。
乔纳森·威克里

12
+1乔纳森。例如,如果move构造函数为,则调整向量的大小将移动对象,而不是复制它们noexcept。因此,“从不”是不正确的。
mfontanini 2012年

4
我的意思是,在这种情况下,编译器生成更好的代码。OP要求提供一个示例,使编译器能够为其生成更优化的应用程序。似乎是这种情况(即使它不是编译器优化)。
mfontanini 2012年

7
@mfontanini:编译器只能生成更好的代码,因为编译器被迫编译不同的codepath。这只是工作,因为std::vector被写入强制编译器来编译不同的代码。这与编译器检测不到什么有关;这是关于用户代码检测到的东西。
Nicol Bolas 2012年

3
问题是,在您开始回答时,似乎无法在报价单中找到“编译器优化”。就像@ChristianRau所说的那样,编译器生成了更有效的代码,而该优化的起源无关紧要。毕竟,编译器正在生成更有效的代码,不是吗?PS:我从未说过这是编译器优化,甚至我说过“这不是编译器优化”。
mfontanini

22

Bjarne的话说(The C ++ Programming Language,第4版,第366页):

在终止是可以接受的响应的情况下,未捕获的异常将实现该目标,因为它变成了对terate()的调用(第1.3.5.2.5节)。而且,noexcept说明符(第1.3.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()中的s)由实现定义 。该程序即将终止,因此我们无论如何都不应依赖任何对象。通过添加一个noexcept说明符,我们表明我们的代码不是为了应付抛出而编写的。


2
您有此报价的来源吗?
安东·高洛夫

5
@AntonGolov“ C ++编程语言,第4版”第pg。366
Rusty Shackleford

在我看来,这似乎是我实际上应该noexcept每次添加一次,只是我明确希望处理异常。让我们说实话,大多数例外是如此不可能和/或致命,以致救援几乎是不可能或不可能的。例如,在引用的示例中,如果分配失败,则应用程序将几乎无法继续正常工作。
Neonit

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

noexcept这很棘手,因为它是功能接口的一部分。尤其是,如果您正在编写库,则客户端代码可以取决于noexcept属性。以后很难更改它,因为您可能会破坏现有代码。当您实现仅由应用程序使用的代码时,可能不必担心。

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

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

  1. 在哪些情况下应更谨慎地使用noexcept,对于哪种情况我可以摆脱隐含的noexcept(false)?

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

  1. 移动操作(移动赋值运算符和移动构造函数)
  2. 调换操作
  3. 内存解除分配器(运算符删除,运算符delete [])
  4. 析构函数(尽管noexcept(true)除非隐式创建,否则它们是隐式的noexcept(false)

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

这种优化在算法级别上,不依赖于编译器优化。这可能会产生重大影响,尤其是在复制元素昂贵的情况下。

  1. 使用noexcept之后,我何时可以实际期望获得性能改善?特别是,给出一个示例代码,在添加noexcept之后,C ++编译器能够为其生成更好的机器代码。

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

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

有关noexcept,堆栈展开和性能的相关问题将在需要堆栈展开时提供有关开销的更多详细信息。

我还建议斯科特·迈耶斯(Scott Meyers)着书“有效的现代C ++”,“第14项:声明函数,除非它们不会发出异常”,以供进一步阅读。


如果像在Java中那样在C ++中实现异常,那将更有意义,在Java中,您标记的方法可能带有throws关键字而不是noexcept否定。我只是无法获得一些C ++设计选择...
doc

他们把它命名为noexcept,因为throw已被接受。简而言之,几乎throw可以按照您提到的方式使用它,除非他们搞砸了它的设计,使它几乎变得无用-甚至有害。但是我们现在坚持使用它,因为删除它会是一个巨大的变化,几乎没有收益。所以基本上是。noexceptthrow_v2
AnorZaken '16

怎么throw没用?
curiousguy

@curiousguy“ throw”本身(用于抛出异常)很有用,但是不建议使用“ throw”作为异常说明符,甚至在C ++ 17中也将其删除。对于为什么例外符是没有用的原因,看到了这个问题:stackoverflow.com/questions/88573/...
菲利普·克拉森

1
@PhilippClaßen throw()异常说明符没有提供与nothrow?相同的保证?
curiousguy

17

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

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

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


到目前为止,这是最好的答案。您正在向您的方法的用户保证,这是另一种表示您将永远限制实现的方法(没有更改)。感谢您的启发性观点。
AnorZaken '16

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.