C++ 17 對例外處理規格的改動

今天我想要介紹 C++ 17 對「例外處理規格(Exception Specification)」的二個改動:

  1. 移除 throw (ClassName)
  2. noexcept 納入型別系統(Type System)

移除 throw (ClassName) 例外處理規格

在 C++ 98 我們能為一個函式編寫「例外處理規格」,表明一個函式只會拋出指定例外:

#include <stdexcept>

void test() throw (std::logic_error, std::runtime_error) {
  // ... some other code ...
}

以上程式碼裡面的 throw (...) 就是例外處理規格。

然而除了用以保證不會拋出例外的 throw (),例外處理規格沒有實用價值,因為:

  1. 例外處理規格只會在執行期檢查「有沒有其他例外被拋出」。
  2. 若其他例外被拋出,例外處理規格會直接呼叫 std::terminate() 終止程式。

舉例來說,上面的 test() 會被翻譯成下面的程式碼:

#include <exception>
#include <stdexcept>

void test()
try {
  // ... some other code ...
} catch (std::logic_error &) {
  throw;
} catch (std::runtime_error &) {
  throw;
} catch (...) {
  std::terminate();
}

從應用程式使用者的角度來看,呼叫 std::terminate() 絕對不是好事,因此編寫例外處理規格可以說是搬石頭砸自己的腳。另一方面,編譯器必需為例外處理規格生成更多程式碼以在執行期施行更多檢查,這不僅會增加執行檔大小,更會阻礙編譯器施行最佳化。因此不少文章主張不要編寫例外處理規格。C++ 11 也將例外處理規格列為 Deprecated(不建議使用)的功能。C++ 17 則正式移除相關語法。如果仍然有需求,則應比照上述範例將程式改寫為 try { ... } catch (...) { std::terminate(); }

將 noexcept 納入型別系統

noexcept 與例外處理規格會影響函式覆寫(Function Override)。如果覆寫函式的例外處理規格比較寬鬆:

class A {
public:
  virtual void test() noexcept {}
};

class B : public A {
public:
  void test() override {};
};

int main() {
  B b;
}

編譯器會印出錯誤訊息:

$ g++ -std=c++17 override_es_error.cpp
override_es_error.cpp:8:8: error: looser throw specifier for
'virtual void B::test()'
   void test() override {};
        ^~~~
override_es_error.cpp:3:16: error:   overriding
'virtual void A::test() noexcept'
   virtual void test() noexcept {}
                ^~~~

然而在 C++ 17 之前,noexcept 與例外處理規格不是型別系統的一部分,因此我們無法透過型別區分一個函式是否會拋出例外。具體的說,我們無法宣告一個只接受 noexcept 的函式指標:

void test();  // May throw any exception

int main() {
#if 0
  // Syntax error, cannot be compiled.
  typedef void (*fn_ptr1_t)() throw ();
  typedef void (*fn_ptr2_t)() noexcept;
#endif

  using fn_ptr3_t = void (*)() throw ();
  using fn_ptr4_t = void (*)() noexcept;

  // Expects errors, but no errors.
  void (*p0)() throw () = test;
  fn_ptr3_t p3 = test;
  fn_ptr4_t p4 = test;
}

C++ 17 將 noexcept 納入型別系統。我們能宣告以 noexcept 修飾的函式指標,且當我們將沒有 noexcept 的函式指派給以 noexcept 修飾的函式指標時,編譯器會印出錯誤訊息:

void test();  // May throw any exception

int main() {
  typedef void (*fn_ptr1_t)() throw ();
  typedef void (*fn_ptr2_t)() noexcept;
  using fn_ptr3_t = void (*)() throw ();
  using fn_ptr4_t = void (*)() noexcept;

  // All of these cause compilation errors in C++ 17.
  void (*p0)() throw () = test;
  fn_ptr1_t p1 = test;
  fn_ptr2_t p2 = test;
  fn_ptr3_t p3 = test;
  fn_ptr4_t p4 = test;
}

在 C++ 17 之後,「以 noexcept 修飾的函式指標」和「沒有 noexcept 的函式指標」是二個不同的型別,可以與函式多載(Function Overloading)結合:

#include <iostream>

void test(void (*ptr)()) {  // #1
  std::cout << "ptr may throw: "
            << reinterpret_cast<void *>(ptr) << std::endl;
}

void test(void (*ptr)() noexcept) {  // #2
  std::cout << "ptr does not throw: "
            << reinterpret_cast<void *>(ptr) << std::endl;
}

void example_may_throw() {}
void example_no_throw() noexcept {}

int main() {
  test(example_may_throw);
  test(example_no_throw);
}

上述範例定義二個 test 函式。兩者都是接收一個函式指標。兩者差異是 noexcept 飾詞。如果傳入的是 example_may_throw, 則會呼叫 #1。如果傳入的是 example_no_throw,則會呼叫 #2

這個技巧能幫助我們編寫例外安全(Exception-safe)的程式碼。舉例來說,如果有一個類別 A 帶有二個類別 B 資料成員。我們要編寫一個名為 apply 的函式以同時修改二個成員。然而,apply 函式必須保證:如果有例外被拋出,兩個資料成員必須保持原樣。通常我們會這樣寫:

class B {
public:
  B() {}
  B(class B &other) {}
};

void swap(B &lhs, B &rhs) noexcept {
  // ...
}

class A {
private:
  B b0_;
  B b1_;
public:
  void apply(void (*transform)(B &)) {
    B b0_tmp(b0_);
    transform(b0_tmp);

    B b1_tmp(b1_);
    transform(b1_tmp);

    swap(b0_, b0_tmp);
    swap(b1_, b1_tmp);
  }
};

為了避免 transform 在修改 b1_ 的時候拋出例外,我們必須要先複製 b0_b1_,確定 transform 能夠套用於 b0_tmpb1_tmp,最後再使用 swap 代換 b0_b1_

透過 noexcept 飾詞,我們能知道 transform 絕對不會拋出例外,進而省略非必要的拷貝。透過多載 apply 函式,我們提供一個更有效率的版本:

class A {
private:
  B b0_;
  B b1_;
public:
  // Optimized version
  void apply(void (*transform)(B &) noexcept) {
    transform(b0_);
    transform(b1_);
  }

  // Safe version (but with temporary local copy)
  void apply(void (*transform)(B &)) {
    B b0_tmp(b0_);
    transform(b0_tmp);

    B b1_tmp(b1_);
    transform(b1_tmp);

    swap(b0_, b0_tmp);
    swap(b1_, b1_tmp);
  }
};

最後稍微提一下,和回傳型別相似,函式本身的 noexcept 不是 Function Signature(函式簽名)的一部分,因此以下範例會被視為重複定義

// Compilation error: redefinition of 'void test()'
void test() {}
void test() noexcept {}

參考資料

Note

如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。