我什么时候应该使用noexcept?

时间:2012-05-28 16:29:09

标签: c++ exception exception-handling c++11 noexcept

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

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

    我必须考虑是否需要在每个函数声明之后附加noexcept会大大降低程序员的工作效率(坦率地说,这将是一个痛苦的屁股)。在哪些情况下,我应该更加谨慎地使用noexcept,在哪些情况下我可以使用隐含的noexcept(false)

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

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

9 个答案:

答案 0 :(得分:162)

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

  

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

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

  

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

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

在大4中使用noexcept(构造函数,赋值,而不是析构函数,因为它们已经noexcept)可能会导致最佳改进,因为noexcept检查在模板中是“常见的”代码,例如std容器。例如,std::vector将不会使用您的类的移动,除非它被标记为noexcept(否则编译器可以推断它)。

答案 1 :(得分:122)

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

添加noexceptnoexcept(true)noexcept(false)首先是语义。它只是偶然地对许多可能的优化进行了调整。

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


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

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

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

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

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

答案 2 :(得分:69)

这确实会对编译器中的优化器产生(可能)巨大差异。编译器实际上已经通过函数定义之后的空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()的边缘对于捕获声明。然后,它可以执行称为“常量传播”的操作,以生成更高效的代码。这里如果内联baz,使用x的语句也可能包含常量,那么以前的运行时评估可以转换为编译时评估等。

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

答案 3 :(得分:48)

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

答案 4 :(得分:30)

  

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

嗯,永远不?永远不是时候?从不。

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

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

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

一般情况下,如果您认为实际上有用,则应使用noexcept。如果is_nothrow_constructible对于该类型为真,则某些代码将采用不同的路径。如果您使用的代码可以使用noexcept适当的构造函数。

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

答案 5 :(得分:19)

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

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

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

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

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

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

  1. 移动操作(移动赋值运算符和移动构造函数)
  2. 交换操作
  3. 内存释放器(operator delete,operator delete [])
  4. 析构函数(虽然这些是隐式noexcept(true),除非你制作它们noexcept(false)
  5. 这些函数通常应该是noexcept,并且库实现很可能使用noexcept属性。例如,std::vector可以使用非投掷移动操作,而不会牺牲强大的异常保证。否则,它将不得不回归复制元素(就像在C ++ 98中那样)。

    这种优化是在算法级别上进行的,并不依赖于编译器优化。它可能会产生重大影响,特别是如果要复制的元素很昂贵。

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

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

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

    关于noexcept, stack unwinding and performance的相关问题详细介绍了在需要堆栈展开时的开销。

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

答案 6 :(得分:14)

以Bjarne的话说:

  

终止是可接受的响应,未被捕获的异常   将实现这一点,因为它变成了一个终止()的调用   (§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节)无条件终止。   它不会调用析构函数来调用函数。它是   实现 - 定义是否从范围之间的析构函数   调用thrownoexcept(例如,对于在compute()中的s)。该   程序即将终止,所以我们不应该依赖任何   无论如何对象。 通过添加noexcept说明符,我们会指出我们的   代码没有写入以应对抛出。

答案 7 :(得分:12)

  

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

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

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

答案 8 :(得分:0)

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

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