CppCon 2018: Jason Turner “Applied Best Practices”总结一 :why noexcept?

什么是noexcept

在CppCon 2018上,Jason Turner在talk《Applied Best Practices》中提出总结了几点best practices,我会通过几篇文章做一些笔记,本文章对c++11中noexcept进行一些总结。

noexcept specifier

noexcept specifier: Specifies whether a function could throw exceptions cppreference.com.

noexcept specifier用于指明一个函数是否有可能抛出异常。noexcept specifier如下几种形式:

  • noexcept
  • noexcept(expression)
  • throw()
    其中noexcept等同于noexcept(true),用于指明函数不会抛出异常。而*noexcept(false)*意味着函数可能会抛出异常。第三种形式不需要浪费精力去深究,因为它将在c++20中移除。

C++中的每个函数要么是non-throwing,要么是potentially throwing,判断一个函数是否抛出异常的详细的标准见noexcept specifier.

noexcept operator

noexcept operator是一个用于判断(编译期)指定的表达式是否会抛出异常运算符,它可以和noexcept specifier一起使用通过某些已有信息来指定某个函数是否会抛出异常。

另外noexcept operator的操作数是unevaluated operands,类似于的还有

  • sizeof operator
  • typeid operator
  • require expression
  • decltype specifier

noexcept operator需要编译器的支持,编译器需要按照一定的标准来判断指定的表达式是否会抛出异常,标准如下:

The result is false if the expression contains at least one of the following potentially evaluated constructs:

  • call to any type of function that does not have non-throwing exception specification, unless it is a constant expression
  • throw expression
  • dynamic_cast expression when the target type is a reference type, and conversion needs a run time check
  • typeid expression when argument type is polymorphic class type

In all other cases the result if true. until c++17

注意这些推断都是简单的编译期的分析,没有使用flow-analysis的分析,如下示例代码,最终的结果也是false

int foo() { return 1; }
int goo() noexcept{ return 0; }

int main() {
  std::cout << noexcept(0 > 1 ? foo() : goo()) << '\n';
}

noexcept缘起

noexcept缘起于C++0x中的move constructor,在由David AbrahamsDouglas Gregor写的文章《Rvalue References and Exception Safety》中介绍了move constructor在exception发生时只支持basic guarantee

To resolve this dilemma, the C++ standard library provides a set of exception-safety guarantees that share the burden of producing correct programs between implementers of the standard library and users of the standard library:

[3a] ‘‘Basic guarantee for all operations:’’ The basic invariants of the standard library are maintained, and no resources, such as memory, are leaked.
[3b] ‘‘Strong guarantee for key operations:’’ In addition to providing the basic guarantee, either the operation succeeds, or has no effects. This guarantee is provided for key library operations, such as push_back(), single-element insert() on a list, and uninitialized_copy()
(§E.3.1, §E.4.1).
[3c] ‘‘Nothrow guarantee for some operations:’’ In addition to providing the basic guarantee, some operations are guaranteed not to throw an exception This guarantee is provided for a few simple operations, such as swap() and pop_back() (§E.4.1). Standard-Library Exception Safety

注:Douglas Gregor是boost的早期重要成员,以及clang和swift的主要作者
Rvalue References and Exception Safety》以vector中的push_back为例,描述了move constructorpush_back出现异常时,可能导致的问题。

T* reallocate(T *old_ptr, size_t old_capacity) {
  // #1: allocate new storage
  T* new_ptr = (T*)new char[sizeof(T) * old_capacity * 2];
  
  // #2: try to move the elements to the new storage
  unsigned i  = 0;
  try {
    // #2a: construct each element in the new storage from the correspoding
    // element in the old storage, treating the old elements as rvalues.
    for (; i < old_capacity; ++i)
      new (new_ptr + i) T(std::move(old_ptr[i])); // "move" operation
  } catch(...) {
    // #2b: destory the copies and deallocate the new storage
    for (unsigned v = 0; v < i; ++v)
      new_ptr[v]->~T();
    delete[]((char*)new_ptr);
    throw;
  }

  // #3: free the old storage
  for (i = 0; i < old_capacity; ++i)
    old_capacity[i]->~T();
  delete[] ((char*)old_ptr);
  return new_ptr;
}

注:上述代码来自于《Rvalue References and Exception Safety
push_back的时候存在如下两种情况,

  • size < capacity,不需要重新分配
  • size == capacity,需要重新分配一块更大的内存,然后将原有的元素一一拷贝或移动到新的内存空间中

上述代码示例描述的就是重新分配更大块内存的情况,此时如果元素有move constructor,那么就会调用move construtor,但如果在调用某个move constructor时,出现了异常,那么此时原有内存空间中已经移动过的对象已经处于moved from state而且这几乎是不可逆的,因为当你尝试将对应元素从新内存空间移动回原有内存空间时,move constructor有可能还会出现异常。所以此时,当元素的move constructor可能会出现异常时,push_back只能作出 basic guarantee,例如vector的capacitysize等保持不变,也没有内存泄漏,但此时某些元素对象的状态已经发生改变。整个过程如下图所示:
move constructor
注:关于moved-from state,请参见EXP63-CPP. Do not rely on the value of a moved-from object

所以《Rvalue References and Exception Safety》提出了如下用于解决该问题的方式:

  • 使用concept,例如require NothrowMoveConstructible<T>,如果不满足则退化到copy constructor
  • 提出了noexcept限定符,编译器会静态检查是否满足noexcept属性,如果被noexcept限定的函数会抛出异常,则这个程序是ill-formed的
  • move constructorsdestructors默认noexcept。如果用户需要move constructorsdestructors抛出异常,则需要显示地使用throw表示,STL不会采用这些会抛出异常的move constructor和destructor。

Rvalue References and Exception Safety》的主要贡献如下:

  • 发现了move constructor破坏了STL中strong exception guarantee这个问题
  • noexcept限定符来说明某个函数不会抛出异常,并静态检查该函数是否违反了noexcept这一性质

noexcept的改进

Rvalue References and Exception Safety》存在如下几个问题:

  • 对于每个noexcept限定符,编译器都要进行静态检查,开销是一个问题
  • 静态检查(使用比较简单的程序分析)过于严格,可能会有“误报”,也就是该函数不可能抛异常,但是编译器在分析的时候却认定它可能会抛异常
  • move constructor默认都是noexcept,只是为了在类似于STL场景中保证strong guarantee,但是在某些用户的场景中,basic guarantee也是可以接受的(毕竟move constructor带来的性能提升太吸引人了),所以最好可以在使用的地方进行控制,是否选择采用可能会抛出异常的move constructor

基于此《Allowing Move Constructors to Throw》提出如下改进:

  • 提供了std::move_if_noexcept
  • 提供了operator noexcept,允许用户按需检查(编译期静态检查)某个函数是否会抛出异常。从而避免对所有使用noexcept限定符的函数进行静态检查。

另外在《Allowing Move Constructors to Throw (Rev. 1)》中提出了重要的改进就是如果使用noexcept限定的函数抛出了异常,则调用std::terminate(一般由std::abort实现),并且保证这个异常不会跳出该函数

到此C++11中的noexcept已经初见雏形,noexcept的提出和改进过程可以总结为下面的描述:

If the noexcept feature appears to you incomplete, prepared in a rush, or in need of improvement, note that all C++ Committee members agree with you. The situation they faced was that a safety problem with throwing move operations was discovered in the last minute and it required a fast solution. The current solution does solve the problem neatly: there will be no silently generated throwing moves that will make STL containers silently break contracts. 《Using noexcept
注:noexcept的由来和改进可以参照Using noexcept

The overhead of exception handling

留坑

Zero-overhead deterministic exceptions: Throwing values

When should I really use noexcept?

留坑
就像是否有必要在所有有返回值的函数前面加上*[[nondiscard]]*一样,nonexcept同样存在这个问题,那么何时需要为函数加上noexcept限定符号呢?关于这个问题众说纷纭,还没有确切的答案,例如在什么情况下做什么。相关资料如下:

  • https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Re-noexcept
  • https://stackoverflow.com/questions/10787766/when-should-i-really-use-noexcept
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值