Effective C++ 条款14_不止于此

在资源管理类中小心 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、施行引用计数法。不过其他行为也都可能被实现。

猜你喜欢

转载自blog.csdn.net/weixin_48033173/article/details/109100938