条款15:在资源管理类中提供对原始资源的访问
此条款,依然是针对对象管理资源的补充,内容分为三个部分:
- 为什么要获取原生资源
- 怎么获取原生资源
- 需要注意什么
一、为什么要获取原生资源
取原书的例子:
class Investment {};
Investment* createInvestment();
std::shared_ptr<Investment> pInv(createInvestment());//见条款13
int daysHeld(const Investment* pi);
int days=daysHeld(pInv);
在此处,代码将不能通过编译,究其原因,dayHeld想要使用一个原生Investment*指针,这里却传递了一个shared_ptr类型的对象。
二、怎么获取原生资源
想要获取原生资源,有两种常见的方法来实现它:显示转换和隐式转换。
1、显示转换
shared_ptr和auto_ptr都提供了一个get成员函数来执行显示转换,也就是返回智能指针对象内部的原生指针(的复件),如下:
//get:得到pInv内部的Investment指针
int days=daysHeld(pInv.get());
与此同时,因为有时候获取RAII对象中的原生资源是必要的,一些RAII类的设计者通过提供一个隐式转换函数来顺利达到此目的。
举个例子,考虑下面的字体RAII类,字体对于C API来说是原生数据结构:
FontHandle getFont(); //得到某种字体
void releaseFont(FontHandle fh);//释放字体
//FontHandle资源管理类
class Font
{
public:
explicit Font(FontHandle fh) :f(fh) {}
~Font() { releaseFont(f); }
private:
FontHandle f;//原始字体资源
};
假设有大量的字体相关的C API用于处理FontHandles,因此会有频繁的需求将Font对象转换成FontHandles对象。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
某些程序员会发现显示请求这些转换是如此令人不愉快以至于不想使用RAII类。但是这会增加泄漏字体资源的机会,这正是设计Font类要预防的事情。
2、隐式转换
如果你觉得显式转换不好,可能会增加泄漏内部资源的可能性,那么可以使用隐式转换函数。
事实上像所有的智能指针一样,shared_ptr和auto_ptr也重载了指针的解引用运算符(operator->和operator*),这就允许将其隐式的转换成底层原生指针。
class Investment {
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); // 工厂函数
std::shared_ptr<Investment>
pi1(createInvestment()); // 管理资源
bool taxable1 = !(pi1->isTaxFree()); //由operator->访问资源
// via operator->
...
std::auto_ptr<Investment> pi2(createInvestment()); // 使用auto_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree()); // 由operator*访问资源
另一种办法是令 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,然后才复制它。
上面的程序拥有一个被Font对象 f1管理的FontHandle,但是直接使用f2也可以获得这个FontHandle。这就不好了。例如:当f1被销毁,字体资源被释放,f2就变成了悬挂指针。
三、需要注意什么
1、隐式转换和显示转换如何选择
提供从RAII类对象到底层资源的显示转换(通过一个get成员函数)还是提供隐式转换依赖于设计出来的RAII类需要执行的特殊任务以及使用的场景。
最好的设计看上去要遵守条款18的建议:使接口容易被正确使用,很难被误用。
通常情况下,像get一样的显示转换函数会是更好的选择,因为它减少了类型误转换的机会。
然而有时候,使用隐式类型转换的自然特性会使局面发生扭转。
2、封装和原始资源背道而驰?
函数返回一个RAII类中的原生资源同封装是背道而驰的,这已经发生了。
这不是设计的灾难,RAII类的存在不是用来封装一些东西;他们的存在是用来保证资源的释放会发生。
如果需要,资源封装可以在这个基本功能之上进行实现,但这不是必要的。
此外,一些RAII类将实现的真正封装同底层资源非常松散的封装组合到一块。
举个例子:shared_ptr封装了所有的引用计数,但是仍然可以非常容易的访问它所包含的原生指针。
像一些设计良好的类,它隐藏了客户没有必要看到的东西,但是它提供了客户端确实需要访问的东西。
四、总结
APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理的资源”的办法。
对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。