首页 > 程序开发 > 软件开发 > C++ >

Effective C++读书笔记(9)

2012-02-04

条款14:在资源管理类中小心copying行为Think carefully about copying behaviorin resource-managing classes条款13介绍了作为资源管理类支柱的 Resource Acquisition IsInitialization (RAII) 原则...

条款14:在资源管理类中小心copying行为

Think carefully about copying behaviorin resource-managing classes

条款13介绍了作为资源管理类支柱的 Resource Acquisition IsInitialization (RAII) 原则,并描述了 auto_ptr 和 tr1::shared_ptr 在基于堆的资源上运用这一原则的表现。然而,并非所有的资源都是基于堆的,对于这样的资源,像 auto_ptr 和 tr1::shared_ptr 这样的智能指针往往不适合作为资源掌管者。在这种情况下,有时可能要根据你自己的需要去创建自己的资源管理类。

例如,假设使用 C API 提供的 lock 和 unlock 函数去操纵 Mutex 类型的互斥对象:

void lock(Mutex *pm); // 锁定pm所指的互斥器

void unlock(Mutex *pm); // 将互斥器解除锁定

为了确保不会忘记解锁一个被你加了锁的 Mutex,你希望创建一个类来管理锁。RAII 原则规定了这样一个类的基本结构,也就是“资源在构造期间获得,在析构期间释放”:

class Lock {
public:
explicit Lock(Mutex *pm):mutexPtr(pm)
{ lock(mutexPtr); } // 获得资源

~Lock() { unlock(mutexPtr); } // 释放资源

private:
Mutex *mutexPtr;
};

客户对Lock的用法符合RAII方式:

Mutex m; // 定义互斥器
...
{ // 建立一个区块用来定义critical section
Lock ml(&m); // 锁定互斥器
... // 执行critical section内的操作

} // 在区块最末尾,自动解除互斥器锁定

critical section:每个线程中访问临界资源的那段程序称为临界区(Critical Section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只准许一个线程进入临界区,进入后不允许其他线程进入。不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。

这没什么问题,但是如果一个 Lock 对象被拷贝应该发生什么?

Lock ml1(&m); // 锁定m

Lock ml2(ml1); // 将ml1复制到ml2身上,这会发生什么?

当一个 RAII 对象被拷贝的时候应该发生什么?大多数情况下,你可以从下面各种可能性中挑选一个:

禁止拷贝:在很多情况下,允许 RAII 被拷贝并不合理。当拷贝对一个 RAII 类没有什么意义的时候,你应该禁止它,通过声明拷贝操作为私有。对于 Lock,看起来也许像这样:
class Lock: private Uncopyable { // 禁止复制
public:
... // 如前
};

对底层的资源引用计数:有时我们希望保有资源直到最后一个使用它的对象被销毁。在这种情况下,拷贝一个 RAII 对象应该增加引用这一资源的对象数目, tr1::shared_ptr正是如此。
通常,RAII 类只需要包含一个 tr1::shared_ptr 数据成员就能够实现引用计数的拷贝行为。例如Lock 要使用引用计数,他可能要将 mutexPtr 的类型从 Mutex* 改变为 tr1::shared_ptr<Mutex>。然而tr1::shared_ptr 的缺省行为是当引用计数变为 0 的时候将它删除,但这不是我们要的,我们想要将它解锁,而不是删除。

幸运的是,tr1::shared_ptr 允许指定所谓的"deleter"(删除器)——当引用计数变为 0 时调用的一个函数或者函数对象(这一功能是 auto_ptr 所没有的,auto_ptr 总是删除它的指针)。deleter是 tr1::shared_ptr 的构造函数可有可无的第二参数,所以,代码看起来就像这样:

class Lock {
public:
explicit Lock(Mutex *pm):mutexPtr(pm, unlock)

// 以某个mutex初始化shared_ptr并以unlock函数为删除器
{ lock(mutexPtr.get());}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; };

// 使用shared_ptr替换raw pointer

注意 Lock 类没有声明析构函数。类的析构函数(无论它是编译器生成还是用户定义)会自动调用这个类的non-static成员变量的析构函数(本例为mutexPtr)。当互斥体的引用计数变为 0 时,mutexPtr 的析构函数会自动调用tr1::shared_ptr 的deleter(本例为unlock)。

拷贝底层资源:有时就像你所希望的你可以拥有一个资源的多个副本。在这种情况下,拷贝一个资源管理对象也要同时拷贝被它包覆的资源。也就是说,拷贝资源管理对象需要进行的是“深层拷贝”。
某些标准字符串类型是由“指向heap内存”的指针构成,那内存用来存放字符串的组成字符。这样的字符串对象包含一个指针指向一块heap内存。当一个string 对象被拷贝,这个副本应该由那个指针和它所指向的内存组成。这样的字符串展现深度复制行为。

传递底层资源的所有权。在某些特殊场合,你可能希望确保只有一个 RAII 对象引用一个裸资源(raw resource),而当这个 RAII 对象被拷贝的时候,资源的所有权从被拷贝的对象传递到拷贝对象。就像 Item 13 所说明的,这就是使用 auto_ptr 时“拷贝”的含意。
拷贝RAII 对象必须一并拷贝它所管理的资源,所以资源的拷贝行为决定了 RAII 对象的拷贝行为。
普通的 RAII 类的拷贝行为是:阻止拷贝、引用计数,但其它行为也是有可能的。

条款15:在资源管理类中提供对原始资源的访问

Provide access to raw resources inresource-managing classes

很多 API 直接涉及资源,所以除非你计划坚决放弃使用这样的 API(太不实际),否则,你就要经常绕过资源管理类而直接处理原始资源(raw resources)。

例如使用类似 auto_ptr 或 tr1::shared_ptr 这样的智能指针来保存 createInvestment 这样的 factory 函数的结果,并希望以某个函数处理Investment对象:

std::tr1::shared_ptr<Investment> pInv(createInvestment());

int daysHeld(const Investment *pi); // 返回投资天数

int days = daysHeld(pInv); // 错误!

daysHeld 要求一个Investment* 指针,但是你传给它一个类型为 tr1::shared_ptr<Investment> 的对象。你需要一个将 RAII 类(本例为 tr1::shared_ptr)对象转化为它所包含的原始资源(本例为底部之Investment*)的函数。有两个常规方法来做这件事:显式转换和隐式转换。

tr1::shared_ptr 和 auto_ptr 都提供一个 get 成员函数进行显示转换,也就是说,返回一个智能指针对象内部的原始指针(或它的一个副本):

int days = daysHeld(pInv.get());// 将pInv内的原始指针传给daysHeld

就像几乎所有智能指针一样,tr1::shared_ptr和 auto_ptr 也都重载了指针解引用操作符(operator-> 和 operator*),它们允许隐式转换到底部原始指针:

class Investment { // investment继承体系的根类
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); // factory函数

std::tr1::shared_ptr<Investment> pi1(createInvestment());

bool taxable1 = !(pi1->isTaxFree()); // 经由operator->访问资源
...

std::auto_ptr<Investment>pi2(createInvestment());

bool taxable2 = !((*pi2).isTaxFree()); // 经由operator*访问资源

...

再考虑以下这个用于字体的RAII类(对C CPI而言字体是一种原生数据结构):

FontHandle getFont(); // C API,为求简化略参数

void releaseFont(FontHandle fh); // 来自同一组C API
class Font { // RAII class
public:
explicit Font(FontHandle fh): f(fh){} // 值传递获得资源

~Font() { releaseFont(f); }

private:
FontHandle f; // 原始字体资源
};

假设有大量与字体相关的C API,它们处理的是FontHandle,这就需要频繁地将 Font 对象转换为 FontHandle。Font 类可以提供一个显式的转换函数,比如get:
FontHandle get() const {return f; }

不幸的是,这就要求客户每次与 API 通信时都要调用 get:

void changeFontSize(FontHandle f, intnewSize); // C API

Font f(getFont());
int newFontSize;
...

changeFontSize(f.get(), newFontSize); // 显式地将Font转换为FontHandle

一些程序员可能发现对显式请求这个转换的需求足以令人郁闷而避免使用这个类。另一个可选择的办法是为 Font 提供一个隐式转换函数,转型为FontHandle:
operator FontHandle()const { return f; }

这样就可以使对C API的调用简单自然:

changeFontSize(f, newFontSize); // 隐式转换,与上例作对照

不利的方面是隐式转换增加错误发生机会。例如,一个客户可能会在有意使用 Font 的地方意外地产生一个 FontHandle:

Font f1(getFont());

...

FontHandle f2 = f1;

// 原意是复制一个Font对象,却反而将f1隐式转换为其底部的FontHandle然后才复制

当 f1被销毁,字体将被释放,f2则被悬挂(dangle)。

最好的设计就是坚持“使接口易于正确使用,不易被误用”。通常,类似get的一个显式转换函数是更可取的方式,因为它将意外的类型转换的机会减到最少。而有时候通过隐式类型转换将提高使用的自然性。

RAII类的存在并非为了封装什么东西而是为了确保资源释放这一特殊行为的发生。此外,一些 RAII类将实现的真正封装和底层资源的宽松封装结合在一起如tr1::shared_ptr 封装了它引用计数的全部机制,但它依然提供对它所包含资源的简单访问。就像大多数设计良好的类,它隐藏了客户不需要看到的,但它也让客户确实需要访问的东西可以被利用。

· API 经常需要访问原始资源,所以每一个 RAII 类都应提供取得它所管理资源的方法。

· 访问可以通过显式转换或者隐式转换进行。通常,显式转换更安全,而隐式转换对客户来说更方便。


摘自 pandawuwyj的专栏

相关文章
最新文章
热点推荐