**如何在C++中实现自定义的异常安全 RAII 容器?**

在 C++ 开发中,异常安全是不可忽视的重要方面。为了让代码在异常出现时仍保持一致性,常用的手段是 RAII(Resource Acquisition Is Initialization)模式,即通过对象的生命周期管理资源。本文以实现一个简易的自定义 RAII 容器为例,说明如何保证异常安全并兼顾性能。

1. 背景与目标

  • 目标:实现一个名为 SafeVector 的包装类,封装标准容器 std::vector,在任何成员函数或外部调用抛出异常时,均能自动回收已分配的资源,避免泄漏。
  • 要求
    1. 支持常见的向量操作(push_back、pop_back、size、operator[])。
    2. 对外提供友好的错误信息。
    3. 保持 O(1) 的插入与删除性能。
    4. 对异常安全做到“基本保证”(即已成功操作的状态保持不变,未完成的操作不影响整体状态)。

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、迭代器等高级特性,以满足更复杂场景的需求。

发表评论