我什么时候应该使用noexcept?

voi*_*ter 472 c++ exception-handling exception noexcept c++11

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

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

    不得不考虑我是否需要noexcept每个函数声明之后追加,这将大大降低程序员的工作效率(坦率地说,这将是一个痛苦的屁股).对于哪些情况,我应该更加小心使用noexcept,以及在哪些情况下我可以使用暗示noexcept(false)

  2. 我何时才能真实地期望在使用后观察到性能提升noexcept?特别是,给出一个代码示例,C++编译器在添加之后能够生成更好的机器代码noexcept.

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

Pub*_*bby 171

我认为现在提供"最佳实践"答案为时尚早,因为在实践中没有足够的时间来使用它.如果在它们出现之后立即询问了抛出说明符,那么答案将与现在非常不同.

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

好吧,然后使用它,显然该功能永远不会抛出.

我何时才能真实地期望在使用后观察到性能提升noexcept?[...]就个人而言,我关心的是noexcept因为提高了编译器安全地应用某些优化的自由度.

似乎最大的优化收益来自用户优化,而不是编译器优化,因为它可能会检查noexcept和重载.大多数编译器遵循一个无惩罚 - 如果你不抛出异常处理方法,所以我怀疑它会在代码的机器代码级别上发生很大变化(或任何事情),尽管可能通过删除处理来减少二进制大小码.

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

  • "好吧,当显然该功能永远不会抛出时,请使用它." 我不同意.`noexcept`是函数*interface*的一部分; 你不应该只是因为你当前的*实现*没有抛出而添加它.我不确定这个问题的正确答案,但我完全相信你的功能在今天的表现如何与它无关...... (23认同)
  • @Pubby C++异常处理通常在没有开销的情况下完成,除了将潜在抛出调用站点地址的跳转映射到处理程序入口点之外.删除这些表与完全删除异常处理完全相同.唯一的区别是可执行文件大小.可能不值得一提. (7认同)
  • 我认为`std :: terminate`技巧仍然遵循零成本模型.也就是说,如果使用`throw`而不是堆栈unwinder,那么`noexcept`函数中的指令范围就会映射到调用`std :: terminate`.因此,我怀疑定期异常跟踪会产生更多开销. (5认同)
  • @Klaim看到这个:http://stackoverflow.com/a/10128180/964135实际上它只是非投掷,但是'noexcept`保证了这一点. (4认同)
  • "如果`noexcept`函数抛出则调用`std :: terminate`,这似乎会涉及少量开销"......不,这应该通过不为这样的函数生成异常表来实现,异常调度程序应抓住然后拯救. (3认同)
  • 现在还太早吗? (3认同)
  • @Potatoswatter:"C++异常处理通常在跳转表之外没有任何开销" - 根据一些编译器作者的说法不正确; 例如,在http://meetingcpp.com/index.php/br/items/insights-into-new-and-c.html搜索"Chandler".知道控制流是线性的编译器当然可以实现一些优化,至少在原理上是这样. (2认同)
  • @NamHoang:1.复制构造函数2.复制赋值3.移动构造函数4.移动赋值 (2认同)

Mat*_* M. 123

我不断重复这些日子:首先是语义学.

添加noexcept,noexcept(true)并且noexcept(false)首先是关于语义.它只是顺便提出了一些可能的优化.

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


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

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

编译器可能只会从异常处理数据中刮掉一些胖(可能),因为它必须考虑到你可能已经撒谎的事实.如果标记的函数noexcept抛出,则std::terminate调用.

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

  • noexcept即使依赖关系不使用它也会立即受益(向后兼容性)
  • 允许指定noexcept何时调用理论上可能抛出但不期望给定参数的函数

  • 是的,可以按照您的建议定义noexcept,但这将是一个真正无法使用的功能.如果某些条件不成立,许多功能都会抛出,即使您知道条件已满足,也无法调用它们.例如,任何可能抛出std :: invalid_argument的函数. (8认同)
  • @马蒂厄M。回复有点晚,但仍然如此。标记为 noexcept 的函数可以调用其他可以抛出的函数,承诺是这个函数不会发出异常,即它们只需要自己处理异常! (4认同)
  • @Nemo:在标准库中,它可能是唯一的,但它展示了一个可以在别处重复使用的原则.移动操作是暂时将某个状态置于"空闲状态"的操作,并且只有当它是"noexcept"时,才有人可以自信地使用它可以在之后访问的数据.我可以看到这个想法在其他地方使用,但标准库在C++中相当薄,它只用于优化我认为的元素副本. (4认同)
  • 我很久以前就赞成这个答案,但是阅读并考虑了一些,我有一个评论/问题."移动语义"是我见过的唯一一个例子,其中"noexcept"显然是有用的/一个好主意.我开始认为移动构造,移动分配和交换是唯一的情况......你知道其他任何人吗? (3认同)
  • 也许我天真,但我想象一个只调用`noexcept`函数的函数不需要做任何特殊的事情,因为任何可能出现的异常会在它们到达这个级别之前触发`terminate`.这与必须处理和传播`bad_alloc`异常有很大不同. (2认同)
  • 或者传递参数以确保被调用者不会抛出即使它对其他输入也不会抛出 - 编译器不够聪明或没有足够的信息来检查,但程序员在美好的一天会做。您并不总是希望根据调用者是否以某种方式预先验证了输入来编写函数的“except”和“noexcept”版本,例如 `operator[]` 与 `at()`。 (2认同)

Ter*_*fey 74

这实际上确实对编译器中的优化器产生了(潜在的)巨大差异.编译器实际上已经通过函数定义之后的空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
}
Run Code Online (Sandbox Code Playgroud)

如果bar标记了此函数的流程图是不同的noexcept(无法执行以bar在catch语句的结尾和catch语句之间跳转).当标记为时noexcept,编译器确定在baz函数期间x的值为5 - x = 5块被称为"支配"baz(x)块而没有边缘bar()到catch语句.然后,它可以执行称为"常量传播"的操作,以生成更高效的代码.如果baz是内联的,那么使用x的语句也可能包含常量,然后可以将运行时评估转换为编译时评估等.

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

  • 我说它在优化包含try/catch块的函数方面确实有所作为.我给出的例子虽然做作,但并非详尽无遗.更重要的一点是,noexcept(就像它之前的throw()语句一样)有助于编译生成一个更小的流程图(更少的边,更少的块),这是它所做的许多优化的基本部分. (6认同)
  • 这在实践中是否真的有所作为?这个例子是设计的,因为*x = 5之前*没有*可以抛出.如果`try`块的那部分用于任何目的,那么推理就不会成立. (3认同)
  • @ qub1n如果编译器可以看到函数的主体,它可以查找显式的`throw`语句,或其他可以抛出的`new`.如果编译器无法看到正文,那么它必须依赖于`noexcept`的存在与否.普通数组访问通常不会生成异常(C++没有边界检查)所以不,数组访问不会单独导致编译器认为函数抛出异常.(越界访问是UB,不是保证的例外.) (3认同)

And*_*zej 51

noexcept可以显着提高某些操作的性能.这不是在编译器生成机器代码的情况下发生的,而是通过选择最有效的算法:如其他人提到的那样,您可以使用函数进行选择std::move_if_noexcept.例如,std::vector(例如,当我们打电话时reserve)的增长必须提供强有力的例外安全保证.如果它知道T移动构造函数没有抛出,它就可以移动每个元素.否则它必须复制所有Ts.这已经详细描述了这个职位.

  • 附录:这意味着如果您定义移动构造函数或移动赋值运算符,则向它们添加"noexcept"(如果适用)!隐式定义的移动成员函数自动添加[`noexcept`](http://stackoverflow.com/a/18654089/3041008)(如果适用). (3认同)

Nic*_*las 30

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

嗯,永远不?永远不是时候?决不.

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

noexcept主要用于允许"你"在编译时检测函数是否可以抛出异常.请记住:大多数编译器不会为异常发出特殊代码,除非它实际上抛出了某些内容.所以noexcept不是给编译器提供关于如何优化函数的提示,而是提供有关如何使用函数的提示.

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

在一般情况下,你应该使用noexcept当你认为它实际上是有用的这样做.如果is_nothrow_constructible该类型为真,则某些代码将采用不同的路径.如果您正在使用可以执行此操作的代码,那么请随意使用noexcept适当的构造函数.

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

  • 严格来说,`move_if_noexcept`不会返回副本,它将返回const左值引用而不是rvalue-reference.通常,这会导致调用者复制而不是移动,但`move_if_noexcept`不会复制.否则,很好的解释. (13认同)
  • +1乔纳森.例如,调整矢量大小将移动对象,而不是在移动构造函数为"noexcept"时复制它们.所以"永远"不是真的. (12认同)
  • @mfontanini:编译器只生成更好的代码,因为编译器被强制编译*不同的代码路径*.它**只是因为写了`std :: vector`来强制编译器编译*不同的*代码.这不是编译器检测到的东西; 它是关于用户代码检测的东西. (7认同)
  • 我的意思是,编译器**将在这种情况下生成更好的代码.OP要求编译器能够生成更优化的应用程序的示例.这似乎是这种情况(即使它不是*编译器*优化). (4认同)
  • @NicolBolas 我觉得你真的在这里争论语义。我知道编译器并不能够生成更好的代码。我也知道你在概念上想说什么,并且争论语义在 C++ 中并不是一件坏事。但说实话,“move_if_noexcept”*正是*问题正在寻找的东西。如果严格来说,*编译器*或*库*并不重要,因为从实际角度来看,对于任何实际上不*开发*C++标准库的用户来说,两者都是相同的*在这种情况下*,这很难任何人都这样做。 (4认同)
  • 问题是,我似乎无法在你的答案开头的报价中找到"编译器优化".正如@ChristianRau所说,编译器生成更高效的代码,优化的起源并不重要.毕竟,编译器*是*生成更高效的代码,不是吗?PS:我从来没有说过这是编译器优化,我甚至说"这不是编译器优化". (3认同)
  • @mfontanini:“毕竟是由编译器生成的。” 按照这种逻辑,使用更好的算法是编译器优化。* Anything *是编译器优化。因此,该术语没有有用的含义。 (2认同)

Phi*_*ßen 20

  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后,我何时能够真实地期望观察到性能提升?特别是,给出一个代码示例,在添加noexcept之后,C++编译器能够生成更好的机器代码.

noexcept无异常规范的优点还是throw()标准允许编译器在堆栈展开时更自由.即使在这种throw()情况下,编译器也必须完全展开堆栈(并且它必须以与对象结构完全相反的顺序执行).

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

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

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


Sau*_*ahu 19

在Bjarne的话:

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

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

  • @AntonGolov"The C++ Programming Language,4th Edition"pg.366 (5认同)
  • 这可能是一个幼稚的问题,但为什么重点放在“vector&lt;double&gt; tmp(10);”上?如果没有足够的内存,上面一行中的字符串实例创建是否会同样抛出异常? (4认同)
  • 对我来说,这听起来好像我实际上应该每次都添加一个“noexcept”,除非我明确想要处理异常。说实话,大多数例外情况都是不太可能发生和/或致命的,以至于救援几乎不合理或不可能。例如,在引用的示例中,如果分配失败,应用程序将很难继续正常工作。 (3认同)
  • 您有此报价的来源吗? (2认同)

Rae*_*ald 15

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

当你说"我知道[他们]永远不会抛出"时,你的意思是通过检查函数的实现,你知道该函数不会抛出.我认为这种方法是内在的.

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