资源管理类(resource-managing classes)很棒。它们是你对抗资源泄漏的堡垒。排除此等泄漏是良好设计系统的根本性质。在一个完美世界中你将倚赖这样的classes来处理和资源之间的所有互动,而不是玷污双手直接处理原始资源(rawresources)。但这个世界并不完美。许多APIs直接指涉资源,所以除非你发誓(这其实是一种少有实际价值的举动)永不录用这样的APIs,否则只得绕过资源管理对象(resource-managing objects)直接访问原始资源(raw resources)。
举个例子,条款13 导入一个观念:使用智能指针如auto ptr或tr1 : :shared ptr保存factory函数如createInvestment的调用结果:
std::tr1::shared_ptr pInv(createInvestment());
假设你希望以某个函数处理Investment对象,像这样:
int daysHeld(const Investment* pi); // 返回投资天数
你想要这么调用它:
int days = daysHeld(pInv); // 错误
却通不过编译,因为daysHeld需要的是Investment*指针,你传给它的却是个类型为tr1::shared_ ptr
这时候你需要一个函数可将RAII class对象(本例为tr1:: shared ptr)转换为其所内含之原始资源(本例为底部之Investment*)。有两个做法可以达成目标:显式转换和隐式转换。
tr1::shared_ptr和 auto_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件):
int days = daysHeld(Pinv.get()); // 很好,将pInv内的原始指针传给daysHeld
就像(几乎)所有智能指针一样,tr1::shared ptr和 auto_ptr也重载了指针取值(pointer dereferencing)操作符(operator->和operator*),它们允许隐式转换至底部原始指针:
class Investment { // investment继承体系的根类
public:
bool isTaxFree() const;
// ...
};
Investment* createInvestment(); // factory函数
std::tr1::shared_ptr pi1(createInvestment); // 令tr1::shared_ptr管理一笔资源
bool taxable1 = !(pi1->isTaxFree()); // 经由operator->访问资源
// ...
std::auto_ptr pi2(createInvestment()); // 令auto_ptr管理一笔资源
bool taxable2 = !((*pi2).isTaxFree()); // 经由operator*访问资源
// ...
由于有时候还是必须取得 RAII对象内的原始资源,某些RAII class设计者于是联想到“将油脂涂在滑轨上”,做法是提供一个隐式转换函数。考虑下面这个用于字体的RAlI class(对 C API而言字体是一种原生数据结构):
FontHandle getFont(); // 这是个C API,简化参数
void releaseFont(FontHandle fh); // 来自同一组C API
class Font {
public:
explicit Font(FontHandle fh) // 获得资源
:f(fh) // pass by value
{}
~Font() { releaseFont(f); } // 释放资源
private:
FontHandle f; // 原始数据
};
假设有大量与字体相关的CAPI,它们处理的是FontHandles,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。Font class可为此提供一个显式转换函数,像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);
某些程序员可能会认为,如此这般地到处要求显式转换,足以使人们倒尽胃口,不再愿意使用这个class,从而增加了泄漏字体的可能性,而Font class 的主要设计目的就是为了防止资源(字体)泄漏。
另一个办法是令Font提供隐式转换函数,转型为FontHandle:
class Font {
public:
// ...
operator FontHandle() const // 隐式转换函数
{ return f; }
// ...
};
这使得客户调用CAPI时比较轻松且自然:
Font f(getFont());
int newFontSize;
// ...
changeFontSize(f, newFontSize); // 将Font隐式转换为FontHandle
但是这个隐式转换会增加错误发生机会。例如客户可能会在需要Font时意外创建一个FontHandle:
Font f1(getFont());
// ...
FontHandle f2 = f1;
以上程序有个FontHandle由 Font对象f1管理,但那个FontHandle也可通过直接使用f2取得。那几乎不会有好下场。例如当f1被销毁,字体被释放,而f2因此成为“虚吊的”( dangle) 。
是否该提供一个显式转换函数(例如get成员函数)将RAII class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAIl class被设计执行的特定工作,以及它被使用的情况。最佳设计很可能是坚持条款18的忠告:“让接口容易被正确使用,不易被误用”。通常显式转换函数如 get是比较受欢迎的路子,因为它将“非故意之类型转换”的可能性最小化了。然而有时候,隐式类型转换所带来的“自然用法”也会引发天秤倾斜。
你的内心也可能认为,RAll class内的那个返回原始资源的函数,与“封装”发生矛盾。那是真的,但一般而言它谈不上是什么设计灾难。RAII classes并不是为了封装某物而存在;它们的存在是为了确保一个特殊行为——资源释放——会发生。如果一定要,当然也可以在这基本功能之上再加一层资源封装,但那并非必要。此外也有某些RAII classes结合十分松散的底层资源封装,藉以获得真正的封装实现。例如tr1 : :shared ptr将它的所有引用计数机构封装了起来,但还是让外界很容易访问其所内含的原始指针。就像多数设计良好的classes一样,它隐藏了客户不需要看的部分,但备妥客户需要的所有东西。
请记住
APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。