条款15:在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes.
显式转换与隐式转换
所谓资源管理类(resource-managing classes),可以有效的帮助我们去预防资源泄漏。
但是,虽然在理想的情况下,我们希望所有的资源与对象之间的互动都依赖于这样的资源管理类,仍有许多的API会直接去涉及原始资源(raw resource)。
举个例子,在前面的条款13中,我们可以得知:使用智能指针auto_ptr或者tr1::shared_ptr来保存factory函数例如createInvestment的调用结果:
std::tr1::shared_ptr<Investment> pInv(createInvestment());
如果我们希望用某个函数来处理Investment对象:
int dayHeld(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
就像几乎所有的智能指针一样,这些对类指针对象都重载了指针取值(pointer dereferencing)操作符(operator ->和operator *),它们允许隐式转换至底部的原始指针:
class Investment { //investment继承体系的根类
public:
bool isTaxFree () const;
...
};
Investment* createInvestment(); //factory函数
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 class(对于C API而言,字体是一种原始数据结构、即原始资源)
FontHandle getFont(); //这是一个C API
void releaseFont(FontHandle fh); //来自同一组的C API
class Font { //RAII class
public:
explicit Font(FontHandle fh) : f(fh) //获得资源,采用pass-by-value的方法
{ }
~Font() { releaseFont(f); } //释放资源
private:
FontHandle f; //原始(raw)字体资源
};
假设有大量与字体相关的C API,它们处理的都是FontHandle,那么“将Font对象转换为FontHandle”将会是一件非常繁琐的事情。因此,Font class可以提供一个显式的转换函数,就像上面的get一样:
class Font {
public:
...
FontHandle get() const { return f; } //显式转换函数
...
};
然而,每次用户想要使用API时,都必须要调用get:
void changeFontSize(FontHandle f, int newFontSize); //C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); //明确地将Font转换为FontHandle
另外一种办法则是令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对象,但是因为隐式转换的原因(f2前面的FontHandle),将f1给隐式转换为了FontHandle,然后才执行了复制操作
在上面的程序中,FontHandle由Font对象f1进行管理,但是这个FontHandle也可以直接通过f2进行取得。这样就会引发问题,例如当f1被销毁时,字体会被释放,而f2因此会成为“虚吊的”(dangle)。
综上所说,是否应该提供一个显式转换函数(例如get成员函数)将RAII class转换为其底部资源,还是提供隐式转换,取决于其执行的具体功能,具体说来:
- 让接口容易被正确使用,不易被误用。
因此,通常显式转换函数如get就是比较好的方法,因为:
- 显式转换将非故意的类型转换的发生的可能性最小化了。
关于RAII class,它们并不是为了封装,而是为了确保:资源释放,这一行为,一定会发生。
最后: