异常安全
异常安全保证与RAII
概述
异常安全(Exception Safety)是指代码在异常发生时仍能保持正确状态的程度。C++ 的异常机制允许错误从深层调用栈传播到合适的处理层,但如果中间代码没有正确处理资源释放和状态一致性,就会导致资源泄漏或数据损坏。异常安全的核心工具是 RAII,它确保无论函数如何退出(正常返回、异常抛出或提前 return),资源都能被正确释放。
基础概念
异常安全等级
| 等级 | 说明 | 示例 |
|---|---|---|
| 不抛出保证(noexcept) | 函数保证不抛出异常 | 析构函数、swap 操作 |
| 强保证(Strong) | 操作要么成功,要么状态不变 | 数据库事务 |
| 基本保证(Basic) | 不泄漏资源,对象处于有效状态 | 大多数标准库操作 |
| 无保证 | 异常可能导致资源泄漏或数据损坏 | 应避免 |
异常安全的三个原则
- 资源管理使用 RAII,避免手动 new/delete
- 在修改状态前完成可能抛出异常的操作
- 使用 copy-and-swap 惯用法实现强保证
快速上手
RAII 保证基本异常安全
#include <fstream>
#include <vector>
#include <memory>
// RAII 自动管理资源,即使异常抛出也能正确释放
void processFile(const std::string& path) {
std::ifstream file(path); // RAII:文件自动关闭
std::vector<int> data; // RAII:内存自动释放
auto buffer = std::make_unique<char[]>(1024); // RAII:自动 delete
// 如果此处抛出异常,file、data、buffer 都会正确释放
if (!file) throw std::runtime_error("无法打开文件");
int value;
while (file >> value) {
data.push_back(value); // vector 自动管理内存
}
// 函数正常退出或异常退出,所有资源都会被正确释放
}
noexcept 保证
class SafeVector {
std::vector<int> data_;
public:
// swap 操作标记为 noexcept
void swap(SafeVector& other) noexcept {
data_.swap(other.data_); // vector::swap 不抛出异常
}
// 析构函数隐式 noexcept
~SafeVector() = default;
// push_back 提供强保证
void push_back(int value) {
data_.push_back(value); // 成功则添加,失败则 vector 不变
}
};
// 全局 swap 使用 noexcept
template<typename T>
void swap(SafeVector<T>& a, SafeVector<T>& b) noexcept {
a.swap(b);
}
详细用法
copy-and-swap 惯用法
#include <vector>
#include <utility>
class StringTable {
std::vector<std::string> entries_;
public:
// 强异常安全的赋值运算符
StringTable& operator=(StringTable other) noexcept { // 注意:参数是值传递
swap(*this, other); // 交换内容
return *this; // other 析构时释放旧数据
}
friend void swap(StringTable& a, StringTable& b) noexcept {
using std::swap;
swap(a.entries_, b.entries_);
}
// 强异常安全的添加操作
void addEntry(const std::string& entry) {
std::vector<std::string> new_entries = entries_; // 先拷贝
new_entries.push_back(entry); // 修改副本(可能抛出异常)
entries_ = std::move(new_entries); // 不抛出异常的赋值
}
};
构造函数的异常安全
#include <memory>
class Widget {
std::unique_ptr<int[]> data_;
size_t size_;
public:
// 异常安全的构造函数
explicit Widget(size_t n) try
: data_(std::make_unique<int[]>(n)) // 可能抛出 bad_alloc
, size_(n)
{
// 初始化数据
for (size_t i = 0; i < n; ++i) {
data_[i] = 0;
}
} catch (const std::bad_alloc&) {
// data_ 尚未构造完成,不需要手动释放
// unique_ptr 的 RAII 保证不会泄漏
std::cerr << "内存分配失败" << std::endl;
throw; // 重新抛出
}
};
异常安全与多线程
#include <mutex>
#include <vector>
class ThreadSafeBuffer {
mutable std::mutex mtx_;
std::vector<int> buffer_;
public:
// 异常安全的加锁操作
void add(int value) {
std::lock_guard<std::mutex> lock(mtx_); // RAII 加锁
buffer_.push_back(value); // 如果抛出异常,lock 析构时自动解锁
}
// 异常安全的批量操作
void addBatch(const std::vector<int>& values) {
std::lock_guard<std::mutex> lock(mtx_);
// 先在副本上操作,确保强保证
std::vector<int> temp = buffer_;
for (int v : values) {
temp.push_back(v);
}
buffer_ = std::move(temp); // noexcept 赋值
}
};
常见场景
事务性操作
class Account {
double balance_;
public:
// 强异常安全的转账操作
static bool transfer(Account& from, Account& to, double amount) {
if (from.balance_ < amount) return false;
// 先扣除,再加增
// 如果第二步失败,需要回滚第一步
from.balance_ -= amount;
try {
to.balance_ += amount;
} catch (...) {
from.balance_ += amount; // 回滚
throw;
}
return true;
}
};
资源管理的异常安全
#include <memory>
// 使用 unique_ptr 管理动态数组
class Image {
std::unique_ptr<unsigned char[]> pixels_;
int width_, height_;
public:
Image(int w, int h)
: pixels_(std::make_unique<unsigned char[]>(w * h * 4)) // RGBA
, width_(w)
, height_(h)
{}
// 异常安全的像素操作
void setPixel(int x, int y, unsigned char r, unsigned char g,
unsigned char b, unsigned char a) {
if (x < 0 || x >= width_ || y < 0 || y >= height_) {
throw std::out_of_range("像素坐标越界");
}
int idx = (y * width_ + x) * 4;
pixels_[idx] = r;
pixels_[idx + 1] = g;
pixels_[idx + 2] = b;
pixels_[idx + 3] = a;
}
};
注意事项
- 析构函数绝不能抛出异常,C++11 中析构函数默认为 noexcept
- 在析构函数中如果操作可能失败,应捕获异常并记录日志
- 移动操作应标记为 noexcept,否则容器(如 vector)在扩容时会选择拷贝而非移动
- 不要在构造函数中执行可能失败的操作后再初始化 RAII 成员,应使用成员初始化列表
- 标准库的容器操作提供的基本保证:不会泄漏内存,对象处于可析构状态
进阶用法
scope_guard 模式
#include <functional>
class ScopeGuard {
std::function<void()> cleanup_;
bool active_ = true;
public:
explicit ScopeGuard(std::function<void()> cleanup)
: cleanup_(std::move(cleanup)) {}
~ScopeGuard() { if (active_) try { cleanup_(); } catch (...) {} }
void dismiss() { active_ = false; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
// 使用 scope_guard 确保异常安全
void complexOperation() {
auto* raw = new int[100];
ScopeGuard guard([&]() { delete[] raw; }); // 确保释放
// 复杂操作,可能抛出异常
processRaw(raw);
guard.dismiss(); // 如果成功,由其他机制管理
}
noexcept 与性能
#include <vector>
class MoveOnlyType {
int* data_;
size_t size_;
public:
// noexcept 移动构造函数使 vector 扩容时使用移动而非拷贝
MoveOnlyType(MoveOnlyType&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
MoveOnlyType& operator=(MoveOnlyType&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
~MoveOnlyType() { delete[] data_; }
};
// vector 扩容时会检查移动构造是否 noexcept
// 如果是,使用移动;否则使用拷贝(更安全但更慢)
std::vector<MoveOnlyType> vec;
vec.push_back(MoveOnlyType(100)); // 使用 noexcept 移动