1. 異常安全保證的三種級別

1.1 基本保證(Basic Guarantee)

定義:如果異常被拋出,程序保持有效狀態,不會發生資源泄漏,但對象的確切狀態可能是未指定的。

實踐示例

class BasicGuaranteeExample {
    int* data;
    size_t size;
    
public:
    void modify(size_t index, int value) {
        if (index >= size) {
            throw std::out_of_range("Index out of range");
        }
        
        // 可能拋出的操作
        data[index] = value;
        
        // 如果這裏拋出異常,對象狀態可能不一致
        // 但至少不會泄漏內存
    }
    
    ~BasicGuaranteeExample() {
        delete[] data;
    }
};

1.2 強保證(Strong Guarantee)

定義:如果異常被拋出,程序狀態與調用操作之前完全一致。

實踐模式

class StrongGuaranteeExample {
    std::vector<int> data;
    
public:
    void addValues(const std::vector<int>& newValues) {
        std::vector<int> backup = data;  // 1. 先備份
        try {
            data.insert(data.end(), newValues.begin(), newValues.end());
            // 可能拋出的操作完成後,再提交
        } catch (...) {
            data.swap(backup);  // 2. 異常時恢復
            throw;
        }
    }
    
    // 或者使用RAII方式
    void safeAddValues(const std::vector<int>& newValues) {
        StrongGuaranteeExample temp = *this;
        temp.data.insert(temp.data.end(), newValues.begin(), newValues.end());
        swap(temp);  // 不拋出異常的操作
    }
};

1.3 不拋異常保證(No-throw Guarantee)

定義:操作承諾永遠不會拋出異常。

適用場景

  • 析構函數
  • 移動操作
  • swap函數
  • 釋放資源操作
class NoThrowExample {
    std::unique_ptr<int[]> data;
    
public:
    // 移動構造函數 - 不應該拋出異常
    NoThrowExample(NoThrowExample&& other) noexcept
        : data(std::move(other.data)) {
    }
    
    // 移動賦值操作符
    NoThrowExample& operator=(NoThrowExample&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
        }
        return *this;
    }
    
    // swap函數 - 不應該拋出異常
    friend void swap(NoThrowExample& a, NoThrowExample& b) noexcept {
        using std::swap;
        swap(a.data, b.data);
    }
};

2. Copy-and-Swap慣用法詳解

2.1 基本實現模式

class SafeVector {
private:
    size_t size_;
    int* data_;
    
    // 實現拷貝構造和交換的輔助類
    struct Impl {
        size_t size;
        std::unique_ptr<int[]> data;
        
        explicit Impl(size_t s = 0) 
            : size(s), data(s ? new int[s] : nullptr) {}
            
        Impl(const Impl& other) 
            : size(other.size), data(other.size ? new int[other.size] : nullptr) {
            std::copy(other.data.get(), 
                     other.data.get() + other.size, 
                     data.get());
        }
    };
    
    std::shared_ptr<Impl> pImpl;
    
public:
    // 構造函數
    SafeVector(size_t size = 0) : pImpl(std::make_shared<Impl>(size)) {}
    
    // 拷貝構造函數
    SafeVector(const SafeVector& other) : pImpl(other.pImpl) {}
    
    // 移動構造函數
    SafeVector(SafeVector&& other) noexcept = default;
    
    // 關鍵:copy-and-swap實現賦值操作符
    SafeVector& operator=(SafeVector other) noexcept {  // 傳值!
        swap(*this, other);
        return *this;
    }
    
    // 提供強異常安全的修改操作
    void push_back(int value) {
        auto newImpl = std::make_shared<Impl>(pImpl->size + 1);
        if (pImpl->size > 0) {
            std::copy(pImpl->data.get(), 
                     pImpl->data.get() + pImpl->size,
                     newImpl->data.get());
        }
        newImpl->data[pImpl->size] = value;
        
        // swap不會拋出異常,提供強保證
        pImpl.swap(newImpl);
    }
    
    friend void swap(SafeVector& a, SafeVector& b) noexcept {
        a.pImpl.swap(b.pImpl);
    }
};

2.2 優點分析

  1. 自動異常安全:異常只可能發生在拷貝構造時,此時不影響原對象
  2. 代碼複用:拷貝構造函數和賦值操作符共享邏輯
  3. 自我賦值安全:自然處理自我賦值情況
  4. 強保證:要麼完全成功,要麼完全不影響原對象

3. 移動操作與異常安全

3.1 移動操作的特殊性

class MoveExceptionSafety {
    std::unique_ptr<Resource> resource;
    std::vector<int> data;
    
public:
    // 移動構造函數 - 標記為noexcept
    MoveExceptionSafety(MoveExceptionSafety&& other) noexcept
        : resource(std::move(other.resource))
        , data(std::move(other.data)) {
    }
    
    // 為什麼需要noexcept?
    // 1. STL容器需要知道移動操作是否安全
    // 2. 影響vector等容器的重新分配策略
    
    void demonstrateVectorReallocation() {
        std::vector<MoveExceptionSafety> vec;
        vec.reserve(2);
        vec.emplace_back();
        vec.emplace_back();
        
        // 當vector需要擴容時:
        // - 如果移動構造函數是noexcept:使用移動
        // - 否則:使用拷貝(保證強異常安全)
    }
};

3.2 移動操作的異常安全實現要點

class ComplexResource {
private:
    Resource* res1;
    Resource* res2;
    
    void cleanup() noexcept {
        delete res1;
        delete res2;
        res1 = res2 = nullptr;
    }
    
public:
    // 不安全的移動構造函數
    ComplexResource(ComplexResource&& other) 
        : res1(other.res1), res2(nullptr) {  // 第一個資源已移動
        
        // 如果這裏拋出異常,other處於部分移動狀態!
        res2 = new Resource(*other.res2);  // 假設可能拋出異常
        
        other.res1 = nullptr;
        other.res2 = nullptr;
    }
    
    // 安全的移動構造函數
    ComplexResource(ComplexResource&& other) noexcept
        : res1(nullptr), res2(nullptr) {
        
        // 先轉移所有權到臨時變量
        Resource* temp1 = other.res1;
        Resource* temp2 = other.res2;
        
        // 安全設置原對象為空
        other.res1 = nullptr;
        other.res2 = nullptr;
        
        // 最後設置當前對象
        res1 = temp1;
        res2 = temp2;
    }
};

4. RAII與異常處理細節

4.1 基本的RAII模式

class DatabaseConnection {
private:
    sqlite3* connection;
    
public:
    explicit DatabaseConnection(const std::string& dbPath) 
        : connection(nullptr) {
        
        // 可能拋出異常的操作
        if (sqlite3_open(dbPath.c_str(), &connection) != SQLITE_OK) {
            throw std::runtime_error("Cannot open database");
        }
        
        // 如果這裏拋出異常,析構函數不會被調用!
        // 需要使用RAII包裝器
    }
    
    ~DatabaseConnection() {
        if (connection) {
            sqlite3_close(connection);  // 不會拋出異常
        }
    }
};

4.2 改進的RAII實現

class SafeDatabaseConnection {
private:
    // 使用unique_ptr自定義刪除器
    struct SqliteDeleter {
        void operator()(sqlite3* db) const noexcept {
            if (db) sqlite3_close(db);
        }
    };
    
    std::unique_ptr<sqlite3, SqliteDeleter> connection;
    
    // 輔助函數,在構造過程中清理資源
    void cleanupOnException() noexcept {
        connection.reset();
    }
    
public:
    explicit SafeDatabaseConnection(const std::string& dbPath) {
        sqlite3* rawConnection = nullptr;
        
        try {
            if (sqlite3_open(dbPath.c_str(), &rawConnection) != SQLITE_OK) {
                throw std::runtime_error("Cannot open database");
            }
            
            connection.reset(rawConnection);
            
            // 更多可能拋出異常的設置操作
            setupDatabase();
            
        } catch (...) {
            // 發生異常時確保清理
            if (rawConnection) sqlite3_close(rawConnection);
            throw;  // 重新拋出異常
        }
    }
    
    void setupDatabase() {
        // 可能拋出異常的操作
        if (sqlite3_exec(connection.get(), 
                        "CREATE TABLE IF NOT EXISTS...", 
                        nullptr, nullptr, nullptr) != SQLITE_OK) {
            throw std::runtime_error("Failed to setup database");
        }
    }
    
    // 自動提供正確的拷貝/移動語義
    SafeDatabaseConnection(const SafeDatabaseConnection&) = delete;
    SafeDatabaseConnection& operator=(const SafeDatabaseConnection&) = delete;
    
    SafeDatabaseConnection(SafeDatabaseConnection&&) noexcept = default;
    SafeDatabaseConnection& operator=(SafeDatabaseConnection&&) noexcept = default;
};

4.3 多階段構造的RAII

class Transaction {
private:
    Database& db;
    bool committed = false;
    
public:
    explicit Transaction(Database& dbRef) 
        : db(dbRef) {
        db.beginTransaction();  // 可能拋出異常
    }
    
    // 提交事務
    void commit() {
        if (!committed) {
            db.commitTransaction();
            committed = true;
        }
    }
    
    // 回滾事務(如果未提交)
    ~Transaction() noexcept {
        try {
            if (!committed) {
                db.rollbackTransaction();
            }
        } catch (...) {
            // 析構函數不應該拋出異常!
            // 記錄日誌,但不能傳播異常
            std::cerr << "Error during rollback" << std::endl;
        }
    }
    
    // 禁用拷貝
    Transaction(const Transaction&) = delete;
    Transaction& operator=(const Transaction&) = delete;
};

// 使用示例
void performDatabaseOperations(Database& db) {
    Transaction trans(db);  // RAII對象
    
    // 一系列可能失敗的操作
    db.execute("INSERT INTO ...");
    db.execute("UPDATE ...");
    
    trans.commit();  // 顯式提交
    // 如果異常發生,Transaction析構函數會自動回滾
}

5. 實戰建議與最佳實踐

5.1 異常安全代碼編寫原則

  1. 使用RAII管理所有資源
  2. 優先使用現有標準庫組件(智能指針、容器等)
  3. 在修改對象前進行拷貝或備份
  4. 確保基本操作(swap、移動、析構)不拋出異常
  5. 按照正確的順序執行操作

5.2 異常安全級別選擇指南

class DesignGuidelines {
public:
    // 1. 析構函數:必須提供不拋異常保證
    ~DesignGuidelines() noexcept {
        // 清理資源,但不能拋出異常
    }
    
    // 2. 移動操作:儘量提供不拋異常保證
    DesignGuidelines(DesignGuidelines&&) noexcept;
    DesignGuidelines& operator=(DesignGuidelines&&) noexcept;
    
    // 3. swap函數:必須提供不拋異常保證
    friend void swap(DesignGuidelines&, DesignGuidelines&) noexcept;
    
    // 4. 關鍵業務操作:提供強保證
    void criticalOperation() {
        // 使用copy-and-swap或其他技術
    }
    
    // 5. 查詢操作:提供不拋異常保證
    bool isValid() const noexcept {
        // 簡單的狀態檢查
        return true;
    }
};

5.3 測試異常安全性

#include <exception>
#include <iostream>

struct TestException : std::exception {};

void testExceptionSafety() {
    bool success = false;
    
    try {
        // 創建測試對象
        SafeVector vec(10);
        
        // 強制拋出異常
        throw TestException();
        
        success = true;
    } catch (const TestException&) {
        // 驗證對象狀態
        std::cout << "Exception caught. Checking object state..." << std::endl;
        
        // 應該能夠正常銷燬對象
        // 沒有資源泄漏
    }
    
    if (!success) {
        std::cout << "Test passed: Strong exception safety guaranteed" << std::endl;
    }
}

總結

異常安全是現代C++編程中至關重要但又常被忽視的方面。通過理解三種異常安全保證的區別,掌握copy-and-swap等慣用法,合理設計移動操作,並充分利用RAII模式,我們可以編寫出既安全又高效的代碼。記住,異常安全不是可有可無的特性,而是構建健壯、可靠軟件系統的基石。