Item 14:函式不會拋出例外就宣告為 noexcept (Declare Functions noexcept If They Won't Emit Exceptions)

Overview Table

重點 說明
noexcept 是介面的一部分 呼叫端可查詢並依賴它(重要性如同 const 成員函式),事後移除會破壞 client code
noexcept 更利於優化 編譯器不必維持 runtime stack 可回溯(unwindable)狀態 → 產生較佳目的碼
價值最高的場合 move 操作、swap、記憶體釋放函式(operator delete)、解構子(destructor)
大多數函式是 exception-neutral 自己不丟例外,但會讓被呼叫者的例外「過境」→ 不該宣告 noexcept
不要扭曲實作換 noexcept 只有自然實作就不丟例外才宣告;硬套會讓實作與呼叫端都更複雜
wide vs narrow contract 嚴謹的函式庫設計者通常只把 noexcept 留給**無前置條件(wide contract)**的函式

noexcept 是介面設計,且帶來優化空間

C++98 的 exception specification(列舉可能丟出的例外型別)維護成本高、容易破壞客戶端,C++11 改採黑白二分:函式可能丟例外,或保證絕不丟例外——後者以 noexcept 表達。

int f(int x) throw();   // C++98 風格:不丟例外(已棄用 deprecated)
int f(int x) noexcept;  // C++11 風格:不丟例外

兩者在「例外真的洩漏出去」時的行為不同,直接決定優化空間:

宣告方式 例外洩漏時的行為 優化程度
noexcept stack 可能回溯(possibly unwound)後終止程式 最可優化
throw()(C++98) stack 必定回溯到呼叫端後終止 較不可優化
無例外規格 正常 stack unwinding 較不可優化
noexcept 是長期承諾

宣告後若反悔,選項都很糟:移除 noexcept 等於改變介面,可能破壞客戶端;保留宣告但讓例外逃出,程式會直接 terminate。只有願意長期維持 noexcept 實作時才宣告。

Move 操作與 swap:noexcept 價值最高之處

std::vector::push_back 擴容時要把舊記憶體的元素搬到新記憶體。C++98 用逐一複製,因此享有 strong exception safety guarantee(複製途中丟例外,原 vector 不變)。C++11 若貿然改用 move:第 n+1 個元素 move 時丟例外,已有 n 個元素被搬走、無法安全復原 → 違反既有保證。

因此標準庫採 「move if you can, but copy if you must」 策略——只有 move 操作**已知不丟例外(宣告了 noexcept)**時才以 move 取代 copy:

vw.push_back(w) 觸發擴容
        │
        ▼
元素型別的 move constructor 是 noexcept 嗎?
   (std::move_if_noexcept →
     std::is_nothrow_move_constructible)
        │
   ┌────┴─────┐
   │ 是        │ 否
   ▼           ▼
 move 舊元素   copy 舊元素
 (高效能)    (保住 strong guarantee)
class Widget {
public:
  Widget(Widget&& rhs) noexcept;  // noexcept move ctor:
                                  // vector 擴容時才會用 move 取代 copy
};

std::vector::reservestd::deque::insert 等 C++98 具 strong guarantee 的函式皆採同一策略。這是替 move 操作宣告 noexcept 最強烈的理由

swap 是另一大受益者:STL 演算法與 copy assignment 實作大量倚賴 swap。標準庫的 swap 常是條件式 noexcept(conditionally noexcept)——高層是否 noexcept 取決於低層元素的 swap 是否 noexcept:

template <class T, size_t N>
void swap(T (&a)[N],
          T (&b)[N]) noexcept(noexcept(swap(*a, *b)));  // 陣列 swap:
                                                        // 元素 swap 是 noexcept 才是 noexcept
template <class T1, class T2>
struct pair {
  void swap(pair& p) noexcept(noexcept(swap(first, p.first)) &&
                              noexcept(swap(second, p.second)));
};

→ 你為 Widget 提供的 swap 是否 noexcept,會層層向上傳染Widget 陣列、pair<Widget, ...>……),所以能提供 noexcept 的 swap 就盡量提供

標準只保證「允許」,不保證「必定」

標準庫容器 move 操作的介面規格並未要求 noexcept;只是實作者被允許強化例外規格,實務上常見容器 move 被宣告為 noexcept。不能假設「所有標準容器的 move 一定是 noexcept」。

Exception-neutral、隱含 noexcept 與 contract 設計

大多數函式是 exception-neutral(例外中立):自己不丟例外,但呼叫的函式可能丟,例外會「過境」傳往上層 handler——這類函式永遠不該是 noexcept。所以多數函式沒有 noexcept 是正確的

情況 該怎麼做 原因
exception-neutral 函式 不宣告 noexcept 例外「路過」即違反 noexcept
為了 noexcept 扭曲實作(例外改回傳錯誤碼) 不要做 呼叫端多出檢查分支,複雜度與 runtime 成本可能抵銷優化收益
destructor / operator delete / operator delete[] 不必宣告(隱含 noexcept) C++11 預設隱含 noexcept,寫了無妨但非慣例
narrow contract(有前置條件)的函式 傾向保留不宣告 測試時可能想丟「precondition violated」例外偵錯,noexcept 會使其直接 terminate
wide contract(無前置條件)且確定不丟 放心宣告 noexcept 最單純、安全的適用情境
解構子隱含 noexcept 的唯一例外

若類別的某資料成員(含繼承成員巢狀於其他成員內者)之型別明確宣告其解構子可能丟例外(如 noexcept(false)),該類別解構子才不是隱含 noexcept。此情況極罕見,標準庫中不存在;且若被標準庫使用的物件(在容器中、傳給演算法)之解構子丟出例外,行為是 undefined behavior

編譯器不檢查一致性:noexcept 函式呼叫沒有 noexcept 保證的函式是完全合法的,編譯器通常不警告——對方可能是 C 程式庫(連 std::strlen 都沒宣告 noexcept)或尚未更新的 C++98 程式庫,其文件保證不丟例外即可:

void setup();               // 別處定義、未宣告 noexcept(可能是 C 程式庫)
void cleanup();

void doWork() noexcept {    // 合法:編譯器不會警告
  setup();                  // 依文件保證不丟例外
  // ... 實際工作 ...
  cleanup();
}

Exam/Test Patterns

情境關鍵字 答案
noexcept 與 const 成員函式的共通點 都是函式介面的一部分,呼叫端可依賴
noexcept vs throw() 優化差異 noexcept 不必維持 stack 可回溯 → 最可優化;throw()(已棄用)與無規格皆較差
vector 擴容何時用 move 元素 move ctor 為 noexcept 時(move if you can, but copy if you must)
push_back 不敢用 move 的原因 move 中途丟例外會破壞 strong exception safety guarantee 且無法復原
std::move_if_noexcept 依據什麼判斷 std::is_nothrow_move_constructible type trait(由 noexcept / throw() 宣告決定)
noexcept(noexcept(...)) 語法 conditional noexcept:外層是規格說明符,內層是運算子;典型用於 swap
哪些函式預設隱含 noexcept 解構子記憶體釋放函式(除非成員解構子宣告 noexcept(false)
大多數函式該不該 noexcept 不該——多數是 exception-neutral,需讓例外過境
wide vs narrow contract wide=無前置條件 → 適合 noexcept;narrow=有前置條件 → 保留丟例外偵錯的空間
noexcept 函式呼叫非 noexcept 函式 合法,編譯器通常不警告
noexcept 函式內例外逃出 程式終止(stack 可能只部分回溯)