洛译小筑

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

[ECPP读书笔记 条目15] 要为资源管理类提供对原始资源的访问权

资源管理类的特征是振奋人心的。它构筑起一道可靠的屏障,可有效地防止资源泄漏。能否预防资源泄漏是“系统的设计方案是否优异”的一个基本评判标准。在完美的世界里,你应该依靠资源管理类来完成所有的与资源交互的工作,而永远不要直接访问原始资源。然而世界并不是完美的。由于许多API会直接引用资源,因此除非你发誓不使用这样的API(这样做显得太不实际了),否则,你必须绕过资源管理类,然后在需要的时候及时手工处理原始资源。

举例说,条目13中引入了下面的做法:使用诸如auto_ptr或者tr1::shared_ptr这样的智能指针来保存诸如createInvestment的工厂函数的返回值:

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

                                       // 来自条目13

假设,当你使用Investment对象时,你需要一个这样的函数:

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

你可能希望这样来调用它:

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

但是这段代码无法通过编译:因为daysHeld需要一个原始的Investment*指针,但是你传递给它的对象的类型却是tr1::shared_ptr<Investment>

你需要一个渠道来将一个RAII类的对象(在上面的示例中是tr1::shared_ptr)转变为它所包含的原始资源(比如说,原始的Investment*)。这里实现这一转变有两个一般的方法:显式转换和隐式转换。

tr1::shared_ptrauto_ptr都提供了一个get成员函数来进行显式转换,也就是说,返回一个智能指针对象中的原始指针(的副本):

int days = daysHeld(pInv.get());          // 工作正常,
                                                         // pInv中的原始指针传递给daysHeld

似乎所有的智能指针类,包括tr1::shared_ptrauto_ptr等等,都会重载指针解析运算符(operator->operator*),这便使得你可以对原始指针进行隐式转换:

class Investment {                 // 投资类型的层次结构中
                                                   // 最为根基的类

public:

  bool isTaxFree() const;

  ...

};

 

Investment* createInvestment();   // 工厂函数

 

std::tr1::shared_ptr<Investment> pi1(createInvestment());
                                                  // 使用tr1::shared_ptr管理资源

bool taxable1 = !(pi1->isTaxFree());
                                                  // 通过operator->访问资源

...

 

std::auto_ptr<Investment> pi2(createInvestment());
                                                  // 使用auto_ptr管理资源

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

...

由于某些时刻你需要获取一个RAII对象中的原始资源,所以一些RAII类的设计者使用了一个小手段来使系统正常运行,那就是:提供一个隐式转换函数。举例说,以下是一个C版本API中提供的处理字体的RAII类:

FontHandle getFont();              // 来自一个C版本API
                                                   // 省略参数表以简化代码

 

void releaseFont(FontHandle fh);     // 来自同一个C版本API

 

class Font {                       // RAII

public:
  explicit Font(FontHandle fh)     // 通过传值获取资源
   : f(fh)                          // 因为该C版本API这样做

  {}

  ~Font() { releaseFont(f); }      // 释放资源 

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

};

假设这里有一个大型的与字体相关的C版本API通过FontHandle解决所有问题,那么把Font对象转换为FontHandle的操作将十分频繁。Font类可以提供一个显式转换函数,比如get

class Font {

public:

  ...

  FontHandle get() const { return f; }
                                                   // 进行显式转换的函数

  ...

};

遗憾的是,这样做使得客户在每次与这一API通信时都要调用一次get

void changeFontSize(FontHandle f, int newSize);
                                                   // 来自该C语言API

Font f(getFont());
int newFontSize;

...

changeFontSize(f.get(), newFontSize);
                                                   // 显式转换:从FontFontHandle

一些程序员可能会发现,由于使用这个类要求我们始终提供上述示例中的那种显式转换,这一点很糟糕,足够让他们拒绝使用这个类了。同时这一设计又增加了字体资源泄漏的可能性,这与Font类的设计初衷是完全相悖的。

有一个替代方案,让Font提供一个可隐式转换为Fonthandle的函数:

class Font {

public:

  ...

  operator FontHandle() const { return f; }
                                                    // 进行隐式转换的函数

 

  ...

};

这使得调用这一C版本API的工作变得简洁而且自然:

Font f(getFont());

int newFontSize;

...

 

changeFontSize(f, newFontSize);   // 隐式转换:从FontFontHandle

 

隐式转换会带来一定的负面效应:它会增加出错的可能。比如说,一个客户在一个需要Font的地方意外地创建了一个FontHandle

Font f1(getFont());

...

FontHandle f2 = f1;                // 啊哦!本想复制一个Font对象,
                                                   // 但是却却将f1隐式转换为其原始的
                                                   // FontHandle,然后复制它

现在程序中有一个FontHandle资源正在由Font对象f1来管理,但是仍然可以通过f2直接访问FontHandle资源。这是很糟糕的。比如说,当f1被销毁时,字体就会被释放,f2将无法被销毁。

是为RAII类提供显式转换为潜在资源的方法,还是允许隐式转换,上面两个问题的答案取决于RAII类设计用于完成的具体任务,及其被使用的具体环境。最好的设计方案应该遵循条目18的建议,让接口更容易被正确使用,而不易被误用。通常情况下,定义一个类似于get的显式转换函数是一个较好的途径,应为它可以使非故意类型转换的可能性降至最低。然而,一些时候使用隐式类型转换显得更加自然,人们更趋向于使用它。

你可能已经发现,让一个函数返回一个RAII类内部的原始资源是违背封装性原则的。的确是这样,乍看上去这简直就是设计灾难,但是它实际上并没有那么糟糕。RAII类并不是用来封装什么的,它们是用来确保一些特别的操作能够得以执行的,那就是资源释放。如果需要,资源封装工作可以放在这一主要功能的最顶端,但是这并不是必需的。另外,一些RAII类结合了实现封装的严格性和原始资源封装的宽松性。比如tr1::shared_ptr对其引用计数机制进行了整体封装,但是它仍然为其所包含的原始指针提供了方便的访问方法。就像其它设计优秀的类一样,它隐藏了客户不需要关心的内容,但是它使得客户的确需要访问的部分对其可见。

时刻牢记

API通常需要访问原始资源,所以每个RAII类都应该提供一个途径来获取它所管理的资源。

访问可以通过显式转换或隐式转换来实现。一般情况下,显式转换更安全,而隐式转换对于客户来说更方便。

posted on 2007-05-13 20:54 ★ROY★ 阅读(800) 评论(0)  编辑 收藏 引用 所属分类: Effective C++


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