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 優點分析
- 自動異常安全:異常只可能發生在拷貝構造時,此時不影響原對象
- 代碼複用:拷貝構造函數和賦值操作符共享邏輯
- 自我賦值安全:自然處理自我賦值情況
- 強保證:要麼完全成功,要麼完全不影響原對象
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 異常安全代碼編寫原則
- 使用RAII管理所有資源
- 優先使用現有標準庫組件(智能指針、容器等)
- 在修改對象前進行拷貝或備份
- 確保基本操作(swap、移動、析構)不拋出異常
- 按照正確的順序執行操作
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模式,我們可以編寫出既安全又高效的代碼。記住,異常安全不是可有可無的特性,而是構建健壯、可靠軟件系統的基石。