C++异常安全保证:从理论到实践
【摘要】 1. 异常安全保证的三种级别 1.1 基本保证(Basic Guarantee)定义:如果异常被抛出,程序保持有效状态,不会发生资源泄漏,但对象的确切状态可能是未指定的。实践示例:class BasicGuaranteeExample { int* data; size_t size; public: void modify(size_t index, int va...
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模式,我们可以编写出既安全又高效的代码。记住,异常安全不是可有可无的特性,而是构建健壮、可靠软件系统的基石。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)