Item 14: 谨慎考虑 resource-managing classes(资源管理类)中的拷贝行为
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
Item 13 介绍了作为 resource-managing classes(资源管理类)支柱的 Resource Acquisition Is Initialization (RAII) 原则,并描述了 auto_ptr 和 tr1::shared_ptr 在 heap-based(基于堆)的资源上运用这一原则的表现。然而,并非所有的资源都是 heap-based(基于堆)的,而对于这样的资源,像 auto_ptr 和 tr1::shared_ptr 这样的 smart pointers(智能指针)通常就不像 resource handlers(资源句柄)那样合适。在这种情况下,有时,你很可能要根据你自己的需要去创建你自己的 resource-managing classes(资源管理类)。
例如,假设你使用一个 C API 提供的 lock 和 unlock 函数去操纵 Mutex 类型的 mutex objects(互斥体对象):
void lock(Mutex *pm); // lock mutex pointed to by pm
void unlock(Mutex *pm); // unlock the mutex
为了确保你从不会忘记解锁一个被你加了锁的 Mutex,你案打算创建一个 class 来管理锁。这样一个 class 的基本结构被 RAII 原则规定,通过 construction(构造函数)获取资源并通过 destruction(析构函数)释放它:
class Lock {
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{ lock(mutexPtr); } // acquire resource
~Lock() { unlock(mutexPtr); } // release resource
private:
Mutex *mutexPtr;
};
客户按照惯常的 RAII 风格来使用 Lock:
Mutex m; // define the mutex you need to use
...
{ // create block to define critical section
Lock ml(&m); // lock the mutex
... // perform critical section operations
} // automatically unlock mutex at end
// of block
这没什么问题,但是如果一个 Lock object 被拷贝应该发生什么?
Lock ml1(&m); // lock m
Lock ml2(ml1); // copy ml1 to ml2—what should
// happen here?
这是一个更一般的问题的特定实例,每一个 RAII class 的作者都必须面对它:当一个 RAII object 被拷贝的时候应该发生什么?大多数情况下,你需要从下面各种可能性中挑选一个:
- prohibit copying(禁止拷贝)。在很多情况下,允许 RAII objects 被拷贝是没有意义的。这对于像 Lock 这样 class 很可能是正确的,因为同步原本的“拷贝”很少有什么意义。当拷贝对一个 RAII class 没有什么意义的时候,你应该禁止它。Item 6 解释了如何做到这一点:声明拷贝操作为私有。对于 Lock,看起来可能就像这样:
class Lock: private Uncopyable { // prohibit copying — see
public: // Item 6
... // as before
};
- reference-count the underlying resource(对底层的资源引用计数)。有时人们需要的是保持一个资源直到最后一个使用它的 object 被销毁。在这种情况下,拷贝一个 RAII object 应该增加引用这一资源的 objects 的数目。这也就是被 tr1::shared_ptr 使用的 "copy"(“拷贝”)的含意。
通常,RAII classes 只需要包含一个 tr1::shared_ptr data member(数据成员)就能够实现 reference-counting(引用计数)的拷贝行为。例如,如果 Lock 要使用 reference counting(引用计数),他可能要将 mutexPtr 的类型从 Mutex* 变为 tr1::shared_ptr<Mutex>。不幸的是,tr1::shared_ptr 的缺省行为是当它所指向的东西的 reference count(引用计数)变为零的时候将它删除,但这不是我们想要的。当我们使用完一个 Mutex 后,我们想要将它解锁,而不是将它删除。
幸运的是,tr1::shared_ptr 允许有一个对 "deleter"(当 reference count(引用计数)变为零时调用的一个 function(函数)或者 function object(函数对象))的特殊说明。(这一功能是 auto_ptr 所没有的,auto_ptr 总是删除它的 pointer(指针)。)deleter 是 tr1::shared_ptr 的 constructor(构造函数)的可选的第二个参数,所以,代码看起来就像这样:
class Lock {
public:
explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
: mutexPtr(pm, unlock) // to point to and the unlock func
{ // as the deleterlock(mutexPtr.get()); // see Item 15 for info on "get"
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
}; // instead of raw pointer在这个例子中,注意 Lock class 是如何不再声明一个 destructor(析构函数)的。那是因为它不需要。Item 5 解释了一个 class 的 destructor(析构函数)(无论它是 compiler-generated(编译器生成)的还是 user-defined(用户定义)的)会自动调用这个 class 的 non-static data members(非静态数据成员)的 destructors(析构函数)。在本例中,就是 mutexPtr。但是,当 mutex(互斥体)的 reference count(引用计数)变为零时,mutexPtr 的 destructor(析构函数)会自动调用 tr1::shared_ptr 的 deleter ——在此就是 unlock。(看过这个 class 的源代码的人多半意识到需要增加一条注释表明你并非忘记了 destruction(析构),而只是依赖 compiler-generated(编译器生成)的缺省行为。)
- copy the underlying resource(拷贝底层的资源)。有时就像你所希望的你可以拥有一个资源的多个拷贝,你需要一个 resource-managing class(资源管理类)的唯一理由就是确保当你使用完每一个拷贝之后,它都会被释放。在这种情况下,拷贝一个 resource-managing object(资源管理对象)也应该同时拷贝它所包覆的资源。也就是说,拷贝一个 resource-managing object(资源管理对象)需要实行一次 "deep copy"(“深层拷贝”)。
某些标准 string 类型的实现是由 pointers to heap memory(指向堆内存的指针)构成,那里存储着组成那个 string(字符串)的字符。这样的 strings 的 objects 包含一个 pointers to heap memory(指向堆内存的指针)。当一个 string object 被拷贝,一个拷贝应该由那个指针和它所指向的内存两者组成。这样的 strings 表现为 deep copying(深层拷贝)。
- transfer ownership of the underlying resource(传递底层资源的所有权)。在非常特殊的场合,你可能希望确保只有一个 RAII object 引用一个 raw resource(裸资源),而当这个 RAII object 被拷贝的时候,资源的所有权从 copied object(被拷贝对象)传递到 copying object(拷贝对象)。就像 Item 13 所说明的,这就是被 auto_ptr 使用的 "copy"(“拷贝”)的含意。
copying functions(拷贝函数)(copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符))可能由编译器生成,所以除非 compiler-generated(编译器生成)的版本所做的事正是你想要的(Item 5 说明了这些缺省行为),否则你应该自己编写它们。在某些情况下,你还要支持这些函数的泛型化版本。这样的版本在 Item 45 中描述。
Things to Remember
- 拷贝一个 RAII object 必须拷贝它所管理的资源,所以资源的拷贝行为决定了 RAII object 的拷贝行为。
- 通常的 RAII class 的拷贝行为是 disallowing copying(不允许拷贝)和 performing reference counting(实行引用计数),但是其它行为也是有可能的。