在资源管理类中小心 copying 行为
条款 13 导入这样的观念:“ 资源取得时机便是初始化时机 ” (Resource Acquisition Is Initialization;RAII),并以此作为 “ 资源管理类 ” 的脊柱,也描述了 auto_ptr 和 tr1::shared_ptr 如何将这个观念表现在 heap-based (基于堆)资源上。
然而,并非所有的资源都是 heap-based,对于那些资源,上面那样的指挥指针往往并不适合作为资源掌管着。
既然如此,我们有时候就需要建立自己的资源管理类。
eg:
假设使用 C API 函数处理类型为 Mutex 的互斥器对象(mutex objects),共有 lock 和 unlock 两种函数可用:
void lock(Mutex* ptm); // 锁定 ptm 所指的互斥器
void unlock(Mutex* ptm); // 解除互斥器锁定
为确保每一个被锁住的 Mutex 解锁,你可能会希望建立一个 class 来管理机锁。
这样的 class 基本结构由 RAII 守则支配:资源在构造期间获得,在析构期间释放。
class Lock{
public:
explicit Lock(Mutex* ptm): mutexPtr(pm){
lock(mutexPtr); // 获得资源
}
~Lock(){
unlock(mutexPtr); } // 释放资源
private:
Mutex* mutexPtr;
};
客户对 Lock 的用法符合 RAII 方式:
Mutex n; // 定义需要的互斥器
...
{
// 建立一个区块用来定义 critical section(关键部分)
Lock ml(&n); // 锁定互斥器
... // 执行 critical section 内的操作
}
这 very nice,但如果 Lock 对象被复制,会发生啥?
Lock ml1(&n); // 锁定n
Lock ml2(ml1); // ml1 复制到 ml2 身上,结果怎样。
这是一个常见的问题:当一个 RAII 对象被复制,会发生什么事?大多是情况有以下两种选择:
- 禁止复制。很多时候允许 RAII 对象被复制并不合理。因为很少能够合理拥有 “ 同步化基础器物 ” 的附件(副本)。所以我们应该禁止它。
条款 06 告诉了我们该怎么做:将 copying 操作声明为 private。对 Lock 而言是这样的:
class Lock: private Uncopyable{
// 禁止复制。见条款 06
...
};
- 对底层资源祭出 “ 引用计数法 ”(reference-count)。有时候我们希望保有资源,直到使用它的最后一个对象被销毁。这种情况下是可以复制 RAII 对象的,并且应该将资源的 “ 被引用数 ” 递增。tr1::shared_ptr 便是如此。
通常只要内含一个 tr1::shared_ptr 成员变量,RAII classes 便可实现 reference-counting copying。
如果前述的 Lock 打算使用 reference counting,它可以改变 mutexPtr 的类型,将它从 Mutex* 改为 tr1::shared_ptr<\Mutex>.(此处<>内没有\,由于无法 Mutex 为 Markdown 内置标签无法显示,故添上 " \ ")。
然而很不幸 tr1::shared_ptr 的缺省行为是 “ 当引用次数为 0 时删除所指之物 ”,那不是我们想要的行为。当我们用上一个 Mutex,我们想要做的释放动作是解除锁定而非删除。
幸运的是 tr1::shared_ptr 允许指定所谓的 “ 删除器 ”(deleter),那是一个函数或函数对象,当引用次数为 0 时便被调用(auto_ptr 则不具备这样的功能——它总是将其指针删除)。删除器对 tr1::shared_ptr 构造函数而言是可有可无的第二参数,所以代码看起来像这样:
class Lock{
public:
explicit Lock(Mutex* ptm): mutexPtr(ptm, unlock){
// 以某个 Mutex 初始化 shared_ptr 并以 unlock 函数为删除器
lock(mutexPtr.get()); // 条款15谈 get()
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; // 使用 shared_ptr 替换 raw(未加工) pointer
};
请注意,本例的 Lock class 不再声明析构函数。class 析构函数会自动调用其 non-static 成员变量(mutexPtr)的析构函数,而 mutexPtr 的析构函数会在互斥器的引用次数为 0 时自动调用 tr1::shared_ptr 的删除器(本例为 unlock)。细想你会发现:你并没有忘记析构,你只是倚赖了编译器生成的缺省行为。
请记住:
- 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
- 普遍而常见的 RAII class copying 行为是:抑制 copying、施行引用计数法。不过其他行为也都可能被实现。