当前位置: 首页 > news >正文

item14--谨慎考虑资源管理类的拷贝行为

1. 问题的核心场景

假设你需要管理一个互斥锁(Mutex)。你写了一个 RAII 类来确保锁在退出作用域时自动释放:

class Lock {
public:explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); } // 获取资源~Lock() { unlock(mutexPtr); }                               // 释放资源
private:Mutex* mutexPtr;
};

如果用户写了这样的代码,会发生什么?

Mutex m;
{Lock ml1(&m); // 锁定 mLock ml2(ml1); // 复制 ml1 到 ml2
} // 作用域结束
  • 默认行为:编译器生成的默认拷贝构造函数会执行“浅拷贝”(Shallow Copy)。ml1ml2 指向同一个 Mutex
  • 灾难:当 ml2 析构时,锁被释放。当 ml1 析构时,锁再次被释放。对同一个锁解锁两次通常是未定义行为(崩溃或数据损坏)。

Scott Meyers 提出了四种策略,我们结合现代 C++ 逐一升级这些方案。


2. 策略一:禁止复制(Prohibit Copying)

这是最常见的情况。很多系统资源(如锁、文件句柄、数据库连接)在语义上就是“独占”的,复制它们没有意义。

  • C++98 做法:将拷贝构造函数和赋值操作符声明为 private 且不实现。
  • Modern C++ 做法:使用 = delete

这是更清晰、编译器报错更友好的方式。

class Lock {
public:explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); }~Lock() { unlock(mutexPtr); }// 现代解法:明确删除拷贝操作Lock(const Lock&) = delete;Lock& operator=(const Lock&) = delete;private:Mutex* mutexPtr;
};

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

如果你希望保留资源直到最后一个使用它的对象销毁(例如:只要还有一个对象引用这个文件,文件就不关闭),则使用引用计数。

  • 实现方式:在类中持有一个 std::shared_ptr
  • 关键点shared_ptr 默认行为是 delete 指针。但对于 Mutex,我们不是要销毁它,而是要解锁它。
  • Modern C++ 解法:利用 std::shared_ptr自定义删除器(Custom Deleter)
class Lock {
public:explicit Lock(Mutex* pm)// 初始化 shared_ptr,并指定删除器为 unlock 函数: mutexPtr(pm, [](Mutex* p) { unlock(p); }) {lock(mutexPtr.get());}// 此时不需要自定义析构函数,shared_ptr 会自动处理// 也不需要禁用复制,默认的浅拷贝正是我们想要的(增加引用计数)private:std::shared_ptr<Mutex> mutexPtr;
};

注:这里使用了 Lambda 表达式作为删除器,这是现代 C++ 的惯用写法。

4. 策略三:深度复制(Deep Copy)

这意味着当你复制对象时,你也复制了底层的资源(例如:复制字符串、复制整个堆上的数据结构)。

  • 适用场景std::stringstd::vector 的行为。
  • 现代意义:对于系统资源(如锁、Socket),深度复制通常是不可能的(你不能“克隆”一个操作系统的锁)。所以这一条在资源管理类中较少见。

5. 策略四:转移所有权(Transfer Ownership)—— 现代 C++ 核心升级

这是 Effective C++ 原书中受限于时代(当时只有 auto_ptr)讲得比较晦涩的部分。在现代 C++ 中,这是处理资源的核心手段:移动语义(Move Semantics)

如果你想让资源“独占”,但又想在函数间传递它,你可以禁止复制,但允许移动

  • Modern C++ 解法:使用 std::unique_ptr 或手动实现移动构造函数。
class Lock {
public:explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); }~Lock() { if(mutexPtr) unlock(mutexPtr); }// 1. 禁止复制Lock(const Lock&) = delete;Lock& operator=(const Lock&) = delete;// 2. 允许移动 (Move Constructor)Lock(Lock&& other) noexcept : mutexPtr(other.mutexPtr) {other.mutexPtr = nullptr; // 把源对象的指针置空,防止析构时解锁}// 3. 允许移动赋值 (Move Assignment)Lock& operator=(Lock&& other) noexcept {if (this != &other) {if (mutexPtr) unlock(mutexPtr); // 释放当前的mutexPtr = other.mutexPtr;      // 接管新的other.mutexPtr = nullptr;       // 置空源}return *this;}private:Mutex* mutexPtr;
};// 用法
Lock l1(&m);
Lock l2 = std::move(l1); // l1 失去所有权,l2 接管。l1 析构时什么都不做。

总结:现代 C++ 的最佳决策树

当你编写一个资源管理类(RAII)时,针对 Copying 行为,请按以下逻辑选择:

  1. 最好的解法(不要造轮子):

    如果标准库已经提供了 RAII 包装器,直接用它。

    • 管理锁:用 std::lock_guard (禁止移动和复制) 或 std::unique_lock (支持移动)。
    • 管理内存:用 std::unique_ptrstd::shared_ptr
    • 管理文件:用 std::fstream
  2. 如果必须自己写类(例如封装 legacy C API)

    • 独占资源(如句柄):使用 std::unique_ptr 管理成员变量,或者手动实现移动语义,并用 = delete 禁止复制
    • 共享资源:使用 std::shared_ptr 管理成员变量,并提供自定义删除器

快速对比表

策略 Effective C++ 原文时代 Modern C++ (C++11/14/17/20)
禁止复制 private 声明不实现 / 继承 Uncopyable = delete
共享所有权 手写引用计数 std::shared_ptr + 自定义 Deleter
转移所有权 std::auto_ptr (已废弃) std::unique_ptr / 移动语义 (&&)
锁管理 手写 Lock std::lock_guard / std::scoped_lock

一句话总结 Item 14:

RAII 类的复制行为必须反映其所管理资源的语义。在现代 C++ 中,首选移动语义(Move)代替复制,或者使用 =delete 明确禁止复制。

http://www.jsqmd.com/news/115933/

相关文章:

  • python django flask酒店客房管理系统数据可视化分析系统_gq8885n3--论文md5
  • python django flask鹿幸公司员工食堂在线点餐餐饮餐桌预约管理系统的设计与实现_utcnqqs0--论文
  • error_code
  • 虚拟化初步了解
  • Miloco 深度打通 Home Assistant,实现设备级精准控制
  • 好用的大型牛场水滴粉碎机技术强的
  • set_value
  • 日记1217
  • function的类型擦除
  • function bind
  • 日记12,19
  • Item10--令赋值操作符返回一个
  • Item9--绝不在构造和析构过程中调用虚函数
  • python django flask考研互助交流平台_c62p51fu--论文
  • 日记12.18
  • 离散化遍历
  • Ubuntu上使用VScode创建Maven项目
  • 线程(2)
  • 大规模语言模型的抽象思维与创新能力培养
  • 线程(1)
  • 方达炬〖发明超新技术〗:冰堆技术;冷极冰堆建筑技术;
  • Item6--若不想使用编译器自动生成的函数,就该明确拒绝
  • 我发现LLM解析基因数据优化抗癌药剂量,患者副作用直降40%
  • 日记12.16
  • 论文AIGC查重率高怎么办?6个降AI率工具和技巧,AI率从100%降到3%! - 还在做实验的师兄
  • PCL曲面重建——为一组点云重建凸多边形/凹多边形
  • 信息与关系:涌现的三大核心原则
  • Linux文件权限
  • 28
  • 灵遁者:量子基元理论带来的新观点