例外會將失敗操作的回報與傳播至能夠回應的邊界分開。例外會將控制權轉移至相符的處理常式;控制權向外傳遞時,C++ 會依反向建構順序,摧毀每個離開其範圍且已完整建構的自動物件。因此,資源取得即初始化(RAII)會讓一般與例外清理路徑相同。
void load_configuration(std::filesystem::path path) {
std::ifstream input{path}; // Closed during unwinding.
if (!input) {
throw std::runtime_error{"cannot open configuration"};
}
// Parsing may throw; input still closes.
}
try {
load_configuration("app.conf");
} catch (const std::exception& error) {
report(error.what());
}請以值擲出例外物件,並以 const 參照捕捉多型例外型別。以值捕捉基底例外會切割衍生物件。catch (...) 只應位於能恢復不變量、轉譯錯誤或刻意終止的邊界;不得在狀態損壞時靜默繼續。
展開與 RAII #
擲出例外的建構函式不會產生完成的物件,但其已完整建構的基底與成員子物件仍會執行解構函式。解構函式不得在另一個例外作用中讓例外逸出,因為這會呼叫 std::terminate。因此,解構函式、智慧指標刪除器與回復防護應不擲出例外。清理可能失敗時,請公開一個在解構前回報失敗的明確操作。
class Transaction {
public:
explicit Transaction(Database& db) : db_{db} { db_.begin(); }
void commit() {
db_.commit();
committed_ = true;
}
~Transaction() noexcept {
if (!committed_) {
db_.rollback_noexcept();
}
}
private:
Database& db_;
bool committed_ = false;
};解構函式只會嘗試回復;commit() 仍是可以回報錯誤的操作。每個公開操作都必須定義其失敗後保留的狀態。
例外安全保證 #
操作可以宣告下列其中一種保證:
- 不擲出保證: 不會傳播例外。
- 強保證: 若失敗,可觀察狀態不變。
- 基本保證: 不變量成立且資源不洩漏,但狀態可能變更。
不要僅因型別使用 RAII,就推斷具有超出基本保證的保證。RAII 可防止洩漏,但不會復原被覆寫的值或外部效果。會擲出的變更操作應記錄其保證。
提交、回復與不變量 #
強保證遵循準備與提交結構:在替代狀態中執行可能擲出的工作,接著套用不會擲出的提交。提交通常是 swap、指標替換或資料庫交易提交。
void Account::replace_limits(Limits requested) {
requested.validate(); // May throw; `limits_` is unchanged.
limits_.swap(requested); // Commit must not throw.
}替代狀態成本太高或外部動作無法復原時,請維持基本保證。定義呼叫者可在每次失敗後依賴的不變量,例如「物件仍可使用,且餘額為非負」。除非外部系統提供交易或補償動作,否則不要宣稱不可逆副作用可以回復。
noexcept 是終止契約
#
noexcept 在某些用途上會改變函數型別,並承諾從函數逸出的例外會造成 std::terminate。它不會抑制例外,也不會定義性地使主體更快。當終止是預期回應,且所有被呼叫操作都符合該契約時,才將函數標記為 noexcept。
除非另行宣告,解構函式會隱含為不擲出。只有在呼叫者能在非展開路徑捕捉其例外時,明確使解構函式成為 noexcept(false) 才適當;若另一個例外已在傳播,這仍然不安全。
noexcept 也會影響標準函式庫策略。例如,std::vector 重新配置時,如果元素的移動建構函式不會擲出,便可以在保留強保證的同時移動元素。若移動可能擲出且可用複製,實作可以改為複製。若移動可能擲出且複製不可用,vector 可能無法為該次重新配置提供強保證。這是容器特定的結果,不是虛假標記操作為 noexcept 的理由。
設計例外邊界 #
讓一般程式碼保持例外中立:使用擁有物件取得資源,在區域中保留不變量,並讓失敗抵達知道復原原則的層。當目標錯誤模型需要時,在程序、請求、執行緒或外部函式邊界轉譯例外。絕不要讓 C++ 例外跨越不允許例外的介面。
例外不是驗證的替代品。在變更前驗證前置條件;當失敗屬於正常的區域控制流程時,使用 C++23 std::expected 等回傳值;當呼叫者需要具備可靠清理的非區域傳播時,使用例外。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)