RAII 类的拷贝

例如,假设你使用 C API 提供的 lock 和 unlock 函数去操纵 Mutex 类型的互斥体对象:

void lock(Mutex *pm); 
void unlock(Mutex *pm); 

为了确保你从不会忘记解锁一个被你加了锁的 Mutex,你希望创建一个类来管理锁。RAII 原则规定了这样一个类的基本结构,通过构造函数获取资源并通过析构函数释放它:

class Lock {
public:
  explicit Lock(Mutex *pm)
  : mutexPtr(pm)
  { lock(mutexPtr); } 

  ~Lock() { unlock(mutexPtr); }    

private:
  Mutex *mutexPtr;
};

客户按照 RAII 风格的惯例来使用 Lock:

Mutex m;     
...
{
    Lock ml(&m);            
    ...                        
}                         

这没什么问题,但是如果一个 Lock 对象被拷贝应该发生什么?

Lock ml1(&m);   
Lock ml2(ml1);    

每一个 RAII 类的作者都要面临这样的问题:当一个 RAII 对象被拷贝的时候应该发生什么?大多数情况下,你可以从下面各种可能性中挑选一个:

  • 禁止拷贝。在很多情况下,允许 RAII 被拷贝是没有意义的。这对于像 Lock 这样类很可能是正确的,因为同步的基本要素的“副本”很少有什么意义。当拷贝对一个 RAII 类没有什么意义的时候,你应该禁止它。声明拷贝操作为私有。对于 Lock,看起来也许像这样:
class Lock: private Uncopyable {            
public:                                     
 ...                                       
};
  • 对底层的资源引用计数。有时人们需要的是保持一个资源直到最后一个使用它的对象被销毁。在这种情况下,拷贝一个 RAII 对象应该增加引用这一资源的对象的数目。这也就是使用 shared_ptr 时“拷贝”的含意。

shared_ptr 允许一个 "deleter" 规范——当引用计数变为 0 时调用的一个函数或者函数对象。(这一功能是 auto_ptr 所没有的,auto_ptr 总是删除它的指针。)deleter 是 shared_ptr 的构造函数的可选的第二个参数,所以,代码看起来就像这样:

class Lock {
public:
  explicit Lock(Mutex *pm)       
  : mutexPtr(pm, unlock)       
  {                         
    lock(mutexPtr.get());       
  }
private:
  shared_ptr<Mutex> mutexPtr;  
};  

在这个例子中,注意 Lock 类是如何不再声明一个析构函数的。那是因为它不再需要。在本例中,就是 mutexPtr。但是,当互斥体的引用计数变为 0 时,mutexPtr 的析构函数会自动调用的是 shared_ptr 的 deleter ,在此就是 unlock。

  • 拷贝底层的资源。有时就像你所希望的你可以拥有一个资源的多个副本,唯一的前提是你需要一个资源管理类确保当你使用完它之后,每一副本都会被释放。在这种情况下,拷贝一个资源管理对象也要同时拷贝被它隐藏的资源。也就是说,拷贝一个资源管理类需要完成一次“深拷贝”。

某些标准 string 类型的实现是由堆内存的指针组成,堆内存中存储着组成那个 string 的字符。这样的字符串对象包含指向堆内存的指针。当一个 string 对象被拷贝,这个副本应该由那个指针和它所指向的内存组成。这样的 string 表现为深拷贝。

  • 传递底层资源的所有权。在某些特殊场合,你可能希望确保只有一个 RAII 对象引用一个裸资源,而当这个 RAII 对象被拷贝的时候,资源的所有权从被拷贝的对象传递到拷贝对象。
总结
  • RAII 类的拷贝构造函数处理机制:
    • 禁止拷贝
    • 对底层资源使用引用计数,可以使用 shared_ptr 的 deleter 机制
    • 对底层数据成员进行深拷贝
    • 传递底层资源的所有权