Effective C++ 条款14:在资源管理类中小心 copying 行为

复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定了 RAII 对象的 copying 行为。普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权。

一、问题的核心

当我们设计一个 RAII(资源获取即初始化)类时,一个关键问题浮现出来:

当 RAII 对象被复制时,它管理的资源应该如何处理?

资源的不同特性决定了不同的 copying 策略。让我们通过一个文件句柄管理类来理解这个问题:

class FileHandle {
public:
    explicit FileHandle(const char* filename)
        : fd_(fopen(filename, "r")) {
        if (!fd_) throw std::runtime_error("Failed to open file");
    }
    
    ~FileHandle() { if (fd_) fclose(fd_); }
    
    // 问题来了:拷贝构造函数和赋值运算符该怎么实现?
    // FileHandle(const FileHandle& rhs);  // ???
    // FileHandle& operator=(const FileHandle& rhs);  // ???

private:
    FILE* fd_;
};

如果我们不定义 copying 函数,编译器会生成默认版本,执行浅拷贝——两个 FileHandle 对象将持有同一个 FILE*。当它们析构时,同一个文件句柄会被 fclose 两次,导致未定义行为!

二、四种普遍的 copying 策略

策略一:抑制 copying(Prohibit Copying)

适用场景:资源不应该被复制,如文件句柄、锁、网络连接等。

class FileHandle {
public:
    explicit FileHandle(const char* filename)
        : fd_(fopen(filename, "r")) {
        if (!fd_) throw std::runtime_error("Failed to open file");
    }
    
    ~FileHandle() { if (fd_) fclose(fd_); }
    
    // 显式删除拷贝操作
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // 允许移动(C++11)
    FileHandle(FileHandle&& other) noexcept
        : fd_(other.fd_) {
        other.fd_ = nullptr;
    }
    
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (fd_) fclose(fd_);
            fd_ = other.fd_;
            other.fd_ = nullptr;
        }
        return *this;
    }

private:
    FILE* fd_;
};

特点:

特性 说明
拷贝 禁止
移动 允许(转移所有权)
资源释放 只有一个所有者负责释放
类似标准类 std::unique_ptr

策略二:引用计数(Reference Counting)

适用场景:资源可以被多个对象共享,直到最后一个使用者释放它。

#include <memory>

class SharedResource {
public:
    explicit SharedResource(int* data) 
        : data_(data), ref_count_(new size_t(1)) {}
    
    // 拷贝构造函数 - 增加引用计数
    SharedResource(const SharedResource& rhs)
        : data_(rhs.data_), ref_count_(rhs.ref_count_) {
        ++(*ref_count_);
    }
    
    // 赋值运算符
    SharedResource& operator=(const SharedResource& rhs) {
        if (this != &rhs) {
            release();  // 释放当前资源
            data_ = rhs.data_;
            ref_count_ = rhs.ref_count_;
            ++(*ref_count_);
        }
        return *this;
    }
    
    ~SharedResource() { release(); }
    
    size_t use_count() const { return *ref_count_; }

private:
    void release() {
        if (--(*ref_count_) == 0) {
            delete data_;
            delete ref_count_;
        }
    }
    
    int* data_;
    size_t* ref_count_;
};

现代 C++ 简化版:

#include <memory>

class ModernShared {
public:
    explicit ModernShared(int value) 
        : data_(std::make_shared<int>(value)) {}
    
    // 编译器生成的拷贝构造函数和赋值运算符
    // 自动处理 shared_ptr 的引用计数
    ModernShared(const ModernShared&) = default;
    ModernShared& operator=(const ModernShared&) = default;
    
    size_t use_count() const { return data_.use_count(); }

private:
    std::shared_ptr<int> data_;
};

特点:

特性 说明
拷贝 允许,引用计数 +1
资源释放 最后一个对象析构时释放
线程安全 引用计数操作需要同步
类似标准类 std::shared_ptr

策略三:深度拷贝(Deep Copy)

适用场景:每个对象应该拥有资源的独立副本,如字符串、容器等。

class DeepCopyBuffer {
public:
    explicit DeepCopyBuffer(size_t size)
        : size_(size), data_(new char[size]) {}
    
    // 拷贝构造函数 - 深拷贝
    DeepCopyBuffer(const DeepCopyBuffer& rhs)
        : size_(rhs.size_), data_(new char[rhs.size_]) {
        std::copy(rhs.data_, rhs.data_ + rhs.size_, data_);
    }
    
    // 赋值运算符
    DeepCopyBuffer& operator=(const DeepCopyBuffer& rhs) {
        if (this != &rhs) {
            // copy-and-swap 惯用法
            DeepCopyBuffer temp(rhs);
            swap(temp);
        }
        return *this;
    }
    
    ~DeepCopyBuffer() { delete[] data_; }
    
    void swap(DeepCopyBuffer& other) noexcept {
        using std::swap;
        swap(size_, other.size_);
        swap(data_, other.data_);
    }

private:
    size_t size_;
    char* data_;
};

特点:

特性 说明
拷贝 允许,创建资源副本
独立性 每个对象有自己的资源副本
开销 拷贝成本较高
类似标准类 std::string, std::vector

策略四:转移底部资源拥有权(Transfer Ownership)

适用场景:只有一个对象应该拥有资源,拷贝时转移所有权给新对象(旧对象不再拥有)。

C++11 之前 std::auto_ptr 采用此策略,现已被 std::unique_ptr 取代。

// C++98 风格的 auto_ptr(已废弃,仅供理解)
template<typename T>
class AutoPtr {
public:
    explicit AutoPtr(T* ptr = nullptr) : ptr_(ptr) {}
    
    // 拷贝构造函数 - 转移所有权!
    AutoPtr(AutoPtr& rhs) : ptr_(rhs.ptr_) {
        rhs.ptr_ = nullptr;  // 原对象失去所有权
    }
    
    // 赋值运算符 - 转移所有权
    AutoPtr& operator=(AutoPtr& rhs) {
        if (this != &rhs) {
            delete ptr_;
            ptr_ = rhs.ptr_;
            rhs.ptr_ = nullptr;
        }
        return *this;
    }
    
    ~AutoPtr() { delete ptr_; }
    
    T* get() const { return ptr_; }
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }

private:
    T* ptr_;
};

问题:这种"拷贝"语义非常反直觉——拷贝后原对象被修改了!这导致了很多 bug,因此 std::auto_ptr 已被废弃。

现代 C++ 的正确做法:使用移动语义

#include <memory>

// C++11 起使用 unique_ptr + 移动语义
std::unique_ptr<int> ptr1(new int(42));
// std::unique_ptr<int> ptr2 = ptr1;  // 错误!不能拷贝
std::unique_ptr<int> ptr2 = std::move(ptr1);  // 正确!显式转移
// 现在 ptr1 为 nullptr,ptr2 拥有资源

三、四种策略对比

策略 拷贝语义 资源释放时机 适用场景 标准库对应
抑制 copying 禁止 唯一所有者析构 文件句柄、锁 std::unique_ptr
引用计数 共享 最后一个析构 共享数据、缓存 std::shared_ptr
深度拷贝 独立副本 各自析构 字符串、容器 std::string
转移所有权 移动(C++11) 新所有者析构 独占资源转移 std::unique_ptr(移动)

四、实际应用场景

场景1:互斥锁(抑制 copying)

#include <mutex>

class LockGuard {
public:
    explicit LockGuard(std::mutex& m) : mutex_(m) { mutex_.lock(); }
    ~LockGuard() { mutex_.unlock(); }
    
    // 锁不能被复制
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;

private:
    std::mutex& mutex_;
};

// 使用
std::mutex mtx;

void threadSafeFunction() {
    LockGuard lock(mtx);  // 加锁
    // ... 临界区 ...
}  // 自动解锁

场景2:图像共享缓存(引用计数)

#include <memory>

class ImageCache {
public:
    using ImagePtr = std::shared_ptr<class Image>;
    
    ImagePtr loadImage(const std::string& path) {
        auto it = cache_.find(path);
        if (it != cache_.end()) {
            return it->second;  // 返回共享的图像
        }
        
        auto img = std::make_shared<Image>(path);
        cache_[path] = img;
        return img;
    }
    
    void clearUnused() {
        for (auto it = cache_.begin(); it != cache_.end(); ) {
            if (it->second.use_count() == 1) {  // 只有缓存持有
                it = cache_.erase(it);
            } else {
                ++it;
            }
        }
    }

private:
    std::map<std::string, ImagePtr> cache_;
};

场景3:矩阵类(深度拷贝)

class Matrix {
public:
    Matrix(size_t rows, size_t cols)
        : rows_(rows), cols_(cols), data_(new double[rows * cols]()) {}
    
    // 深拷贝
    Matrix(const Matrix& rhs)
        : rows_(rhs.rows_), cols_(rhs.cols_),
          data_(new double[rhs.rows_ * rhs.cols_]) {
        std::copy(rhs.data_, rhs.data_ + rows_ * cols_, data_);
    }
    
    Matrix& operator=(const Matrix& rhs) {
        if (this != &rhs) {
            Matrix temp(rhs);
            swap(temp);
        }
        return *this;
    }
    
    // 移动构造(C++11)
    Matrix(Matrix&& other) noexcept
        : rows_(other.rows_), cols_(other.cols_), data_(other.data_) {
        other.data_ = nullptr;
        other.rows_ = other.cols_ = 0;
    }
    
    ~Matrix() { delete[] data_; }
    
    void swap(Matrix& other) noexcept {
        using std::swap;
        swap(rows_, other.rows_);
        swap(cols_, other.cols_);
        swap(data_, other.data_);
    }

private:
    size_t rows_, cols_;
    double* data_;
};

场景4:网络连接池(移动语义)

class NetworkConnection {
public:
    explicit NetworkConnection(const std::string& endpoint);
    ~NetworkConnection() { if (socket_ != -1) close(socket_); }
    
    // 禁止拷贝
    NetworkConnection(const NetworkConnection&) = delete;
    NetworkConnection& operator=(const NetworkConnection&) = delete;
    
    // 允许移动
    NetworkConnection(NetworkConnection&& other) noexcept
        : socket_(other.socket_), endpoint_(std::move(other.endpoint_)) {
        other.socket_ = -1;
    }
    
    NetworkConnection& operator=(NetworkConnection&& other) noexcept {
        if (this != &other) {
            if (socket_ != -1) close(socket_);
            socket_ = other.socket_;
            endpoint_ = std::move(other.endpoint_);
            other.socket_ = -1;
        }
        return *this;
    }
    
    void send(const std::vector<uint8_t>& data);

private:
    int socket_;
    std::string endpoint_;
};

// 连接池返回可移动但不可拷贝的连接
class ConnectionPool {
public:
    NetworkConnection acquire() {
        if (!available_.empty()) {
            auto conn = std::move(available_.back());
            available_.pop_back();
            return conn;  // 移动返回
        }
        return NetworkConnection("default");
    }
    
    void release(NetworkConnection conn) {
        available_.push_back(std::move(conn));
    }

private:
    std::vector<NetworkConnection> available_;
};

五、设计决策流程

当你设计 RAII 类时,按以下流程选择 copying 策略:

资源是否可被共享?
├── 否 → 资源是否可拷贝?
│   ├── 否 → 抑制 copying,提供移动语义(如文件句柄)
│   └── 是 → 深度拷贝(如字符串、矩阵)
└── 是 → 引用计数(如共享缓存、图像资源)

六、常见陷阱

陷阱1:忘记处理 copying 函数

class BadRAII {
public:
    explicit BadRAII(int* p) : ptr_(p) {}
    ~BadRAII() { delete ptr_; }
    // 糟糕!没有定义拷贝构造函数和赋值运算符
    // 编译器生成的默认版本会浅拷贝,导致 double free
private:
    int* ptr_;
};

BadRAII a(new int(42));
BadRAII b = a;  // 两个对象持有同一指针!
// a 和 b 析构时都会 delete 同一个指针 → UB!

陷阱2:混合使用不同策略

// 不要这样做:有时深拷贝,有时浅拷贝
class Confusing {
public:
    Confusing(const Confusing& rhs) {
        if (some_condition) {
            // 深拷贝
        } else {
            // 浅拷贝?
        }
    }
};

陷阱3:在引用计数中忘记处理自我赋值

SharedResource& operator=(const SharedResource& rhs) {
    // 错误!没有处理自我赋值
    release();
    data_ = rhs.data_;
    ref_count_ = rhs.ref_count_;
    ++(*ref_count_);
    return *this;
}

// 正确做法
SharedResource& operator=(const SharedResource& rhs) {
    if (this != &rhs) {
        release();
        data_ = rhs.data_;
        ref_count_ = rhs.ref_count_;
        ++(*ref_count_);
    }
    return *this;
}

七、现代 C++ 最佳实践

1. 优先使用标准库智能指针

// 独占所有权
std::unique_ptr<Resource> exclusive;

// 共享所有权
std::shared_ptr<Resource> shared;

// 弱引用
std::weak_ptr<Resource> weak;

2. 使用 Rule of Zero / Rule of Five

// Rule of Zero:如果类只包含可以正确管理的成员,让编译器生成所有函数
class ModernClass {
    std::unique_ptr<int> data_;  // 编译器生成的拷贝/移动/析构都正确
};

// Rule of Five:如果需要自定义其中一个,通常需要定义全部五个
class CustomClass {
public:
    CustomClass();                                    // 默认构造
    ~CustomClass();                                   // 析构
    CustomClass(const CustomClass&);                  // 拷贝构造
    CustomClass& operator=(const CustomClass&);       // 拷贝赋值
    CustomClass(CustomClass&&) noexcept;              // 移动构造
    CustomClass& operator=(CustomClass&&) noexcept;   // 移动赋值
};

3. 使用显式默认和删除

class ExplicitControl {
public:
    ExplicitControl() = default;
    ~ExplicitControl() = default;
    
    ExplicitControl(const ExplicitControl&) = delete;            // 禁止拷贝
    ExplicitControl& operator=(const ExplicitControl&) = delete;
    
    ExplicitControl(ExplicitControl&&) = default;                // 允许移动
    ExplicitControl& operator=(ExplicitControl&&) = default;
};

八、总结

请记住:

  • 复制 RAII 对象必须一并复制它所管理的资源
  • 资源的 copying 行为决定了 RAII 对象的 copying 行为
  • 普遍的 RAII class copying 行为:
    1. 抑制 copying:资源不可共享(std::unique_ptr
    2. 引用计数:资源可共享(std::shared_ptr
    3. 深度拷贝:每个对象需要独立副本(std::string
    4. 转移所有权:C++11 移动语义(std::unique_ptr 移动)
  • 优先使用标准库智能指针,避免重复造轮子

RAII 类的 copying 行为设计是 C++ 资源管理的核心。选择正确的策略不仅能保证程序正确性,还能提升代码的可维护性和性能。现代 C++ 的标准库已经提供了成熟的解决方案,理解这些策略的原理,才能在需要自定义资源管理类时做出正确的设计决策。


参考阅读:

  • 《Effective C++》第3版,Scott Meyers,条款14
  • 《Effective Modern C++》,Scott Meyers
  • C++ Core Guidelines: C.21 (Rule of Zero/Five)
  • cppreference.com: 拷贝控制、移动语义
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐