公告:本站提供编程开发方面的技术交流与分享,打造最佳教程网,希望能为您排忧解难!

Effective C++ 第15条:要为资源管理类提供对原始资源的访问权

Effective C++ 第15条:要为资源管理类提供对原始资源的访问权

更新时间:2013-03-16 16:39:58 |

第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_ptr 和 auto_ptr 都提供了一个 get 成员函数来进行显式转换,也就是说,返回一个智能指针对象中的裸指针(的副本):

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

似乎所有的智能指针类,包括 tr1::shared_ptr 和 auto_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);

  // 显式转换:从 Font 到 FontHandle

由于需要显式请求这样的转换,这样做显得得不偿失,一些程序员也许会拒绝使用这个类。然而这又增加了字体资源泄漏的可能性,这与 Font 类的设计初衷是完全相悖的。

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

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

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

Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize);    // 隐式转换:从 Font 到 FontHandle

隐式转换会带来一定的负面效应:它会增加出错的可能。比如说,一个客户端程序员在一个需要 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 类都应该提供一个途径来获取它所管理的资源。
  • 访问可以通过显式转换或隐式转换来实现。一般情况下,显式转换更安全,但是隐式转换对于客户端程序员来说使用更方便。
最佳教程网

最大的技术交流平台 www.goodxyx.com© CopyRight 2011-2013, All Rights Reserved

浙ICP备11033019号