【问题标题】:Does it make sense to declare inline functions noexcept?声明内联函数 noexcept 有意义吗?
【发布时间】:2014-02-23 03:31:55
【问题描述】:

据我所知,SO 社区在声明函数noexcept 是否能够实现有意义的编译器优化方面存在分歧,而这在其他情况下是不可能的。 (我说的是编译器 优化,而不是基于move_if_noexcept 的库实现优化。)出于这个问题的目的,我们假设noexcept 确实使有意义的代码生成优化成为可能。有了这个假设,声明inline 函数noexcept 有意义吗?假设这些函数实际上是内联的,这似乎需要编译器在调用站点的 inline 函数产生的代码周围生成等效的 try 块,因为如果该区域出现异常,terminate必须调用。如果没有noexcepttry 块似乎是不必要的。

我最初的兴趣是声明 Lambda 函数 noexcept 是否有意义,因为它们隐含为 inline,但后来我意识到任何 inline 函数都会出现同样的问题,而不仅仅是 Lambda。

【问题讨论】:

  • A try-block 意味着堆栈展开。调用 terminate 不会。
  • 抛开效率问题不谈,如果函数真的应该是 noexcept,这可能会在情境中有用。
  • @dyp:当我写“相当于try 块”时,我试图表达这样一种想法,即仍然必须生成代码以在运行时检测异常并调用@987654337 @如果他们出现了。我同意生成的代码不需要完全像真正的 try 块那样做。
  • @KnowItAllWannabe 即使内联函数不是noexcept,也可能需要生成等效的try 块。但是,由于 try 块的运行时成本为 0,因此这应该不是问题。
  • @KnowItAllWannabe 首先,您仍然必须在任何局部变量上调用析构函数。第二个:try 块的成本为零,因为如果你有 try 块,或者没有,编译器会生成完全相同的代码。 (至少,任何体面的编译器都会这样做。标准并没有禁止生成额外的代码,但没有理由这样做。)

标签: c++ c++11 inline noexcept


【解决方案1】:

让我们假设 noexcept 确实使有意义的代码生成优化成为可能

好的

假设这些函数实际上是内联的,这似乎 要求编译器生成相当于一个 try 块 调用站点的内联函数产生的代码,因为 如果该地区出现异常

不一定,因为编译器可能会查看函数体并看到它不可能抛出任何东西。因此可以省略名义上的异常处理。

如果函数是“完全”内联的(也就是说,如果内联代码不包含函数调用),那么我希望编译器可以相当普遍地做出这个决定——但不是例如在有调用的情况下到vector::push_back() 并且函数的编写者知道已经保留了足够的空间,但编译器没有。

还要注意,在一个好的实现中,在没有抛出任何东西的情况下,try 块实际上可能根本不需要执行任何代码。

有了这个假设,声明内联函数 noexcept 有意义吗?

是的,为了得到noexcept 的任何假设优化。

【讨论】:

  • 您关于编译器优化try 块的评论通过检查它们的内容而独立于这个问题。编译器总是可以执行这种优化。
  • @KnowItAllWannabe:我认为这与问题有关。如果 try 块的性能成本为零,那么 try 块对于考虑使用 noexcept 是否是一个好主意变得无关,据我所知,这个问题大部分都消失了。当然,即使没有执行任何代码,它也可能不会完全为零成本,因为异常处理可能会使二进制文件膨胀。但是,内联也会使二进制文件膨胀,但通常它会提高性能。
  • 一个更好的不可能异常的例子是bad_alloc on vector::push_back() 在足够的vector::reserve() 之后。对于编译器来说几乎不可能证明,对于人类来说是微不足道的。
【解决方案2】:

值得注意的是,权力圈子里有一场关于 nothrow 相关问题的有趣讨论。我强烈推荐阅读这些:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3227.html

http://www.stroustrup.com/N3202-noexcept.pdf

显然,相当有影响力的人有兴趣在 C++ 中添加某种自动 nothrow 推导。

经过深思熟虑后,我将位置改为几乎相反,见下文

考虑一下:

  • 当您在 声明 上调用具有noexcept 的函数时——您会从中受益(无需处理可展开性等问题)

  • 当编译器编译一个在定义上有noexcept的函数时——(除非编译器可以证明该函数确实是nothrow)性能受到影响(现在编译器需要确保没有异常可以转义这个函数)。您要求它强制执行无例外承诺

noexcept 既伤害你又让你受益。如果函数是内联的,情况就不是这样了!当它被内联时 - noexcept 对声明没有任何好处(声明和定义成为一件事)......除非你真的希望编译器为了安全起见强制执行它。 安全我的意思是你宁愿终止而不是产生错误的结果

现在应该很明显了 -- 声明 inlined 函数 noexcept 没有意义(请记住,并非每个 inline 函数都会被 inlined )。

让我们看看不同类别的不会抛出的函数(你只知道它们不会抛出):

  1. 非内联,编译器可以证明它不会抛出 --noexcept 不会伤害函数体(编译器将简单地忽略规范)并且调用站点将从这个承诺中受益

    李>
  2. 非内联,编译器无法证明它不会抛出 --noexcept 会伤害函数体,但有利于调用站点(很难说什么更有益)

  3. 内联,编译器可以证明它不会抛出 -- noexcept 没有用处

  4. 内联,编译器无法证明它不会抛出 -- noexcept 会伤害调用站点

如您所见,nothrow 只是设计糟糕的语言功能。它仅在您想强制执行无异常承诺时才有效。没有办法正确使用它——它可以给你“安全”,但不能给你性能。

noexcept 关键字最终被用作承诺(声明时)和强制执行(定义时)——我认为这不是一个完美的方法(哈哈,第二次尝试异常规范,我们仍然没有做对)。

那么,该怎么办?

  1. 声明你的行为(唉,语言在这里没有任何帮助)!例如:

    void push_back(int k);   // throws only if there is no unused memory
    
  2. 不要将noexcept 放在内联函数上(除非它不太可能是内联,例如非常大)

  3. 对于非内联函数(或不太可能内联的函数)- 进行调用。较大的函数得到较小的noexcept 的负面影响(相对而言)——在某些时候为调用者的利益指定它可能是有意义的

  4. 在移动构造函数和移动赋值运算符(和析构函数?)上使用noexcept。它可能会对它们产生负面影响,但如果你不这样做 - 某些库函数(std::swap、一些容器操作)将不会采用最有效的路径(或者不会提供最好的异常保证)。基本上任何在你的函数上使用noexcept operator 的地方(截至目前)都会强制你使用noexcept specifier

  5. 如果您不信任您的函数发出的调用,请使用 noexcept,宁可死也不要让它出现意外行为

  6. 纯虚函数——通常你不相信实现这些接口的人。购买保险通常是有意义的(通过指定noexcept

那么,noexcept 还能怎么设计?

  1. 我会使用两个不同的关键字——一个用于声明承诺,另一个用于执行承诺。例如。 noexceptforce_noexcept。事实上,强制并不是真正需要的——它可以通过 try/catch + terminate() 来完成(是的,它会展开堆栈,但谁在乎它后面是否跟着 std::terminate()?)

  2. 我会强制编译器分析给定函数中的所有调用,以确定它是否可以抛出。如果确实如此,并且做出了 noexcept 承诺 - 将发出编译器错误

  3. 对于可以抛出的代码,但你知道它没有,应该有一种方法来确保编译器它是好的。像这样的:

    vector<int> v;
    v.reserve(1);
    ...
    nothrow {       // memory pre-allocated, this won't throw
        v.push_back(10);
    }
    

    如果承诺被破坏(即有人更改了矢量代码,现在它提供了其他保证)- 未定义的行为。

免责声明:这种方法可能太不切实际了,谁知道...

【讨论】:

    猜你喜欢
    • 2016-11-11
    • 1970-01-01
    • 1970-01-01
    • 2014-01-17
    • 2020-10-24
    • 2014-04-02
    • 2012-03-08
    • 1970-01-01
    相关资源
    最近更新 更多