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限定符號呢?關於這個問題衆說紛紜,還沒有確切的答案,例如在什麼情況下做什麼。相關資料如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章