在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes
资源管理类很值得我们去使用,是我们对抗资源泄漏的堡垒。排除此等泄漏是良好设计系统的根本性质。
但尽管如此,仍有很多APIs直接指涉资源,所以除非你永不录用这样的APIs,否则只得绕过资源管理对象直接访问原始资源。
举个例子,之前的条款导入一个观念:使用智能指针如auto_ptr或tr1::shared_ptr保存factory函数createInvestment的调用结果:
std::tr1::shared_ptr<Investment>pInv(createInvestment());
假设你希望以某个函数处理Investment对象,像这样:
int daysHeld(const Investment* pi);
你希望这么调用它:
int days = daysHeld(pInv);
却不通过编译,因为dayHeld需要的是Investment*指针,你传给它的却是个类型为tr1::shared_ptr<Investment>的对象。
这时候你需要一个函数可将RAII class对象(本例为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(); //factory函数
std::tr1::shared_ptr<Investment> //令tr1::shared_ptr管理一笔资源
pi1(createInvestment());
bool taxablel = !(pi1->isTaxFree()); //经由operator->访问资源
...
std::auto<Investment>pi2(createInvestment()); //令auto_ptr管理一笔资源
bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源
由于有时候还是必须取得RAII对象内的原始资源,某些RAII class设计者于是提供一个转换函数。考虑下面这个用于字体的RAII class:
FontHandle getFont(); //这是个C API。为求简化暂略参数
void relaseFont(FontHandle fh); //来自同一组C API
class Font{ //RAII class
public:
explicit Font(FontHandle fh) //获得资源
:f(fh) //采用pass-by-value
{}
~Font() { releaseFont(f );}
private:
FontHandle f; //原始字体资源
}
假设有大量与字体相关的C API,它们处理的是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); //明白地将Font转换为FontHandle
某些程序员可能会认为,如此这般地到处要求显式转换,足以使人们烦躁,不再愿意使用这个class,从而增加了泄漏字体的可能性,而Font class的主要设计目的就是为了防止资源泄漏。
另一个办法是令Font提供隐式转换函数,转型为FontHandle:
class Font{
public:
...
operator FontHandle() const //隐式转换函数
{ return f; }
...
}
/*
隐式转换函数:
operator 类型名()
{
实现转换的语句
}
1.在函数名前面不能指定函数类型,函数没有参数。
2.其返回值的类型是由函数名中指定的类型名来确定的。
3.类型转换函数只能作为成员函数,因为转换的主体是本类的对象,不能作为友元函数或普通函数。
4.从函数形式可以看到,它与运算符重载函数相似,都是用关键字operator开头,只是被重载的是类型名。
*/
这使得客户调用C API时比较轻松且自然:
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); //将Font隐式转换为FontHandle
但是这个隐式转换会增加错误发生机会。例如客户可能会在需要Font时意外创建一个FontHandle:
Font f1(getFont());
...
FontHandle f2 = f1; //原意是要拷贝一个Font对象,却反而
//将f1隐式转换为其底部的FontHand然后才复制它
以上程序有个FontHandle由Font对象f1管理,但那个FontHandle也可通过直接使用f2取得。那几乎不会有好下场。例如当f1被销毁,字体被释放,而f2因此成为“虚吊的”。
是否该提供一个显式转换函数(例如get成员函数)将RAII class转换为其底部资源,或是应该提供隐式转换,答案取决于RAII class被设计执行的特定工作,以及它被使用的情况。通常显式转换函数如get是比较受欢迎的路子,因为它将“非故意之类型转换”的可能性最小化。然而有时候,隐式类型转换所带来的“自然用法”也会引发天秤倾斜。
你的内心也可能认为,RAII class内的那个返回原始资源的函数,与“封装”发生矛盾。那是真的,但一般而言它谈不上是什么设计灾难。RAII class并不是为了封装某物而存在;它们的存在是为了确保一个特殊行为——资源释放——会发生。
总结:
- APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理的资源”的办法
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
编于 03/28/2019 21:15