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 函式中,優化器不必讓 runtime stack 保持可回溯狀態,也不必保證物件依建構的反序解構。
- 明知函式不丟例外卻不宣告 noexcept,是糟糕的介面規格(poor interface specification)。
宣告後若反悔,選項都很糟:移除 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::reserve、std::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(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 可能只部分回溯) |
Related Notes
- 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation — 編譯器生成的 move 操作與解構子何時為 noexcept(Item 17)
- 04-Moving-To-Modern-Cpp/09-Constexpr — 同章兄弟筆記:另一個「介面承諾」關鍵字 constexpr(Item 15)
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward —
std::move與std::move_if_noexcept的基礎(Item 23) - 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — move 語意實務中 noexcept 的影響(Item 25)
- 06-Move-Semantics-And-Perfect-Forwarding/07-Move-Operation-Realities — move 不一定存在、不便宜、不一定被用上——noexcept 缺席即是原因之一(Item 29)
- 09-Tweaks/01-Pass-By-Value — pass-by-value 效益依賴便宜且 noexcept 的 move(Item 41)
- 01-Introduction/01-Terms-And-Conventions — exception safety(basic / strong guarantee)術語基礎