在 C++ 开发中,异常安全是不可忽视的重要方面。为了让代码在异常出现时仍保持一致性,常用的手段是 RAII(Resource Acquisition Is Initialization)模式,即通过对象的生命周期管理资源。本文以实现一个简易的自定义 RAII 容器为例,说明如何保证异常安全并兼顾性能。
1. 背景与目标
- 目标:实现一个名为
SafeVector的包装类,封装标准容器std::vector,在任何成员函数或外部调用抛出异常时,均能自动回收已分配的资源,避免泄漏。 - 要求:
- 支持常见的向量操作(push_back、pop_back、size、operator[])。
- 对外提供友好的错误信息。
- 保持 O(1) 的插入与删除性能。
- 对异常安全做到“基本保证”(即已成功操作的状态保持不变,未完成的操作不影响整体状态)。
2. 设计思路
- 内部存储:使用
std::unique_ptr<T[]>动态数组管理内存,配合size_t记录元素个数。 - 异常安全策略:
- 所有成员函数在修改内部状态前先尝试完成所有可能抛异常的操作。
- 采用“复制并交换”(copy-and-swap)技巧:先在临时对象完成操作,再通过
swap将临时对象与当前对象交换。 - 通过
std::exception_ptr捕获并重新抛出,保证错误被正确传递。
3. 关键实现
#include <memory>
#include <stdexcept>
#include <utility>
#include <algorithm>
#include <iostream>
template<typename T>
class SafeVector {
public:
SafeVector() : data_(nullptr), size_(0), capacity_(0) {}
// 添加元素
void push_back(const T& value) {
ensure_capacity(size_ + 1);
try {
data_[size_] = value; // 可能抛异常
} catch (...) {
// 若赋值失败,capacity 仍然足够,size_ 未改变
throw; // 重新抛出
}
++size_;
}
// 移除最后一个元素
void pop_back() {
if (size_ == 0) throw std::out_of_range("pop_back on empty SafeVector");
--size_; // 异常安全,除非 size_ 计算本身抛异常
}
// 元素访问
T& operator[](size_t idx) {
if (idx >= size_) throw std::out_of_range("Index out of range");
return data_[idx];
}
const T& operator[](size_t idx) const {
if (idx >= size_) throw std::out_of_range("Index out of range");
return data_[idx];
}
size_t size() const noexcept { return size_; }
private:
void ensure_capacity(size_t min_capacity) {
if (min_capacity <= capacity_) return;
size_t new_cap = std::max(capacity_ * 2, size_t(1));
std::unique_ptr<T[]> new_data(new T[new_cap]); // 可能抛异常
// 复制旧数据
std::copy_n(data_.get(), size_, new_data.get()); // 可能抛异常
// 成功后交换
data_.swap(new_data);
capacity_ = new_cap;
}
std::unique_ptr<T[]> data_;
size_t size_;
size_t capacity_;
};
说明
ensure_capacity负责扩容。所有可能抛异常的步骤(内存分配、元素拷贝)都在局部变量中完成,只有在全部成功后才通过swap交换到成员变量。push_back在拷贝赋值后才++size_,避免在赋值异常时修改size_。pop_back只做简单的检查与递减,异常几乎不可能出现。
4. 异常安全级别
| 级别 | 描述 | 实现方式 |
|---|---|---|
| 完全保证 | 任何异常都不改变对象状态 | 通过 copy-and-swap 确保所有修改先在临时对象完成 |
| 基本保证 | 已完成的操作保持不变,未完成的不会影响整体 | 仅在成功后更新 size_,避免部分成功导致不一致 |
本实现属于完全保证级别:无论异常在哪个步骤抛出,调用者看到的 SafeVector 状态始终保持一致。
5. 性能评估
- 插入:平均 O(1),最坏情况 O(n)(扩容时复制)。
- 删除:O(1)。
- 访问:O(1)。
与 std::vector 对比,SafeVector 在正常操作时几乎没有额外开销,主要区别在于异常处理路径更严格。
6. 实际使用示例
int main() {
SafeVector <int> sv;
try {
for (int i = 0; i < 10; ++i) sv.push_back(i);
sv.push_back(5); // 正常
// sv.push_back(std::string("overflow")); // 触发异常
} catch (const std::exception& e) {
std::cout << "异常捕获: " << e.what() << std::endl;
}
std::cout << "Size: " << sv.size() << std::endl; // 10
}
7. 小结
通过上述实现,我们在 C++ 中完成了一个自定义的 RAII 容器 SafeVector,实现了完整的异常安全保证。核心思路是把可能抛异常的操作全部放到临时对象中完成,使用 swap 或者 copy-and-swap 将安全状态迁移到最终对象。这样即使在资源分配或元素拷贝过程中发生异常,程序也能保持一致且无泄漏。未来可以进一步扩展支持 move semantics、迭代器等高级特性,以满足更复杂场景的需求。