洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目14] 要注意资源管理类中的复制行为

条目13中介绍了“资源获取即初始化”(Resource Acquisition Is Initialization,简称RAII)的概念,它是资源管理的中心内容。同时条目13中还使用auto_ptrtr1::shared_ptr作为示例,描述了如何利用这一概念来管理堆上的资源。然而并不是所有的资源都分配于堆上,对于不分配于堆上的资源,诸如auto_ptrtr1::shared_ptr这一类的智能指针并不适合于处理它们。这是千真万确的,你必须不时地自己动手,创建自己的资源管理类。

举例说,你正使用一个C版本的API所提供的lockunlock函数来处理Mutex类型的互斥对象:

void lock(Mutex *pm);              // 通过pm为互斥量上锁

void unlock(Mutex *pm);            // 为互斥量解锁

为了确保你曾上锁的互斥量都得到解锁,你应该自己编写一个类来管理互斥锁。这样的类的基本结构应遵循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);                      // m上锁

 

Lock ml2(ml1);                     // ml1复制给ml2

                                   // 将会发生什么呢?

有一个问题是所有的RAII类创建者必须面对的,那就是:当复制一个RAII对象时应做些什么。以上是对于这个一般化问题的一个较具体的示例。大多数时候,以下四种可行的方案供你选择。

禁止复制。在许多情况下,允许RAII被复制没有任何意义。比如对于Lock类来说就是这样,因为复制同步原型在大多数情况下都没有什么意义。当复制一个RAII类无意义时,你就应该禁止它。条目6中详细介绍了实现方法:将拷贝赋值运算符声明为私有的。对于Lock而言,应该是下面的情形:

class Lock: private Uncopyable {      // 防止复制 参见条目6

public:

 ...                                   // 同上

};

为潜在生成的资源进行引用计数。有时,我们期望能保留对一个资源的所有权,直到其所涉及的最后一个对象被删除为止。在这种情况下,复制一个RAII对象将会添加一个引用资源对象的计数。这就是tr1::shared_ptr所使用的“复制”的含义。

通常情况下,RAII类可以通过包含一个tr1::shared_ptr数据成员来实现引用计数复制行为。举例说,如果Lock在设计时之初就期望使用引用计数,它可能会用tr1::shared_ptr<Mutex>代替Mutex*来作为mutexPtr的类型。但是不幸的是,tr1::shared_ptr默认的行为是:当引用计数值变为零时,删除其所指向的内容,但这不是我们想要的。当一个Mutex用完时,我们希望对其进行的操作是解锁,而不是删除它。

所幸的是,tr1::shared_ptr允许指定一个“删除器”,它是一个函数或一个函数对象,用于在引用计数值为零时进行调用。(auto_ptr并不包含这一特性,它总是删除它所指向的内容。)删除器是tr1::shared_ptr构造函数的第二个(可选的)参数,所以代码应该是这样的:

class Lock {

public:

  explicit Lock(Mutex *pm)     // 初始化shared_ptr,参数为

  : mutexPtr(pm, unlock)       // 指向Mutex的指针和解锁函数

 

    lock(mutexPtr.get());      // 关于"get"的信息请参见条目15

  }

 

private:

  std::tr1::shared_ptr<Mutex> mutexPtr;

};                             // 使用shared_ptr而不是原始指针

在本示例中,请注意Lock类不再声明析构函数。这是因为我们不再需要它了。条目5中介绍了类的析构函数(无论是编译器自动生成的还是用户自定义的)会自动为类的非静态数据成员进行析构。就像本示例中的mutexPtr。然而,当互斥量的引用计数变为零时,mutexPtr将会自动调用tr1::shared_ptr的删除器unlock。(此时如果你为代码添加了一段注释,告诉人们你并没有忘记编写析构函数,你只是借助了默认的编译器行为。人们看了这样的注释思路会更清晰一些。他们会感激你的。)

复制潜在生成的资源。一些时候,你可以在需要的情况下为资源复制出任意份数的副本,此时你需要一个资源管理类的唯一理由就是:确保每份副本在其工作完成之后得到释放。在这种情况下,复制资源管理对象的同时,也要复制出其所涉及的资源。也可以说,复制一个资源管理对象时,将进行“深度复制”。

标准string类型的一些实现版本中,包含着一个指向堆内存的指针,这个指针所指向的就是字符串所保存的位置。这样的string对象包含着一个指向堆内存的指针。当一个string对象被复制时,将同时复制这一指针和其指向的内存。这样的string就进行了一次深度复制。

传递潜在生成资源的所有权。在少数情况下,你可能需要确保仅仅有一个RAII对象引用了一个原始的资源,当复制这一RAII对象时,资源的所有权也将从源对象传递到目标对象。如同条目13中所解释的,这是通过auto_ptr所实现的“复制”的含义。

拷贝函数(拷贝构造函数和拷贝赋值运算符)可以由编译器自动生成,但是如果编译器自动生成版本无法满足你的需要(条目5中解释了C++的默认行为),你就应该自己编写这些函数。在一些情况下,你可能还会需要支持这些函数的一般化的版本。这些版本将在条目45中介绍。

时刻牢记

由于复制一个RAII对象必须要同时复制其所管理的资源,因此资源的复制行为决定RAII对象的复制行为。

RAII类有两种一般性的复制行为:禁止复制和进行资源计数。同时其他的行为也是可能存在的。

posted on 2007-05-11 18:40 ★ROY★ 阅读(931) 评论(1)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【翻译】[Effective C++第三版•中文版][第14条]要留心资源管理类中的复制行为  回复  更多评论   

boost的scoped_ptr使用的第一个策略:禁止复制

:)
2007-05-23 10:15 | recorder

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理