Item 14:资源管理类要特别注意拷贝行为
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 机制
- 对底层数据成员进行深拷贝
- 传递底层资源的所有权