C++ 例外與例外安全

最後更新: 2024-05-28 17:55:25 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

例外會將失敗操作的回報與傳播至能夠回應的邊界分開。例外會將控制權轉移至相符的處理常式;控制權向外傳遞時,C++ 會依反向建構順序,摧毀每個離開其範圍且已完整建構的自動物件。因此,資源取得即初始化(RAII)會讓一般與例外清理路徑相同。

cpp
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。因此,解構函式、智慧指標刪除器與回復防護應不擲出例外。清理可能失敗時,請公開一個在解構前回報失敗的明確操作。

cpp
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() 仍是可以回報錯誤的操作。每個公開操作都必須定義其失敗後保留的狀態。

例外安全保證 #

操作可以宣告下列其中一種保證:

  1. 不擲出保證: 不會傳播例外。
  2. 強保證: 若失敗,可觀察狀態不變。
  3. 基本保證: 不變量成立且資源不洩漏,但狀態可能變更。

不要僅因型別使用 RAII,就推斷具有超出基本保證的保證。RAII 可防止洩漏,但不會復原被覆寫的值或外部效果。會擲出的變更操作應記錄其保證。

提交、回復與不變量 #

強保證遵循準備與提交結構:在替代狀態中執行可能擲出的工作,接著套用不會擲出的提交。提交通常是 swap、指標替換或資料庫交易提交。

cpp
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 上閱讀或評論(發表評論需要登入)