1.问题的引入
在一个完美的世界里,资源管理类会帮你完成对资源的所有操作,自己不用关心资源管理类里面的原始资源。但现实是残酷的,有时候仍然需要自己直接接触资源管理类所封装的原始资源。
在之前的文章C++中基于对象来管理资源中我们使用智能指针shared_ptr保存工厂函数createInvestment()的调用结果,如下所示:
1std::shared_ptr<Investment> pInv(createInvestment());
假设现在你希望用某个函数daysHeld()去处理Investment对象,如果你想像下面那样调用它,就会编译不通过。因为daysHeld()函数需要的参数是Investment*指针,但你传给它的参数却是shared_ptr<Investment>类型的对象。如下所示:
1int daysHeld(const Investment* pi); 2int days = daysHeld(pInv);
2.解决方法
为了解决上面的问题,你需要一个函数去将RAII类的对象(shared_ptr)转换为其内部所含有的原始资源(Investment*)。有两个方法可以完成目标:显式转换和隐式转换。
方法1:显式转换
shared_ptr和auto_ptr都提供了一个get成员函数,用来执行显示转换。即它会返回智能指针内部的原始指针。如下所示:
1int days = daysHeld(pInv.get());
方法2:隐式转换
像所有的智能指针一样,shared_ptr和auto_ptr也重载了指针的解引用运算符(->和*),它们允许隐式转换底层原始指针。
1class Investment{ 2public: 3 bool isTaxFree() const; 4 // ... 5 6}; 7 8Investment* createInvestment(); // 工厂函数用来返回指向Investment对象的指针 9 10std::shared_ptr<Investment> pi1(createInvestment()); // 令shared_ptr管理一笔资源 11bool taxable1 = !(pi1->isTaxFree()); // 经由->去访问资源 12// ... 13 14std::auto_ptr<Investment> pi2(createInvestment()); // 令auto_ptr管理一笔资源 15bool taxable2 = !((*pi2).isTaxFree()); // 经*去访问资源
3.其他情形
有时候,我们需要把RAII资源管理类的对象所封装的原始资源拿出来,可以定义一个隐式转换函数,将资源管理类隐式或显式转换为原始资源。例如,要实现对C API中的字体类型(font)的资源管理。
1FontHandle getFont(); // C API定义的分配字体函数 2void releaseFont(FontHandle fh); // C API定义的释放字体函数
下面,我们定义自己的RAII资源管理类Font,如下所示:
1class FontHandle{ 2 // ... 3}; 4 5FontHandle getFont(); // C API定义的分配字体函数 6void releaseFont(FontHandle fh); // C API定义的释放字体函数 7 8class Font{ 9public: 10 explicit Font(FontHandle fh): f(fh) // C只能使用值传递 11 { 12 // 构造时获取资源 13 } 14 ~Font(){ 15 releaseFont(f); 16 } 17private: 18 FontHandle f; 19};
如果我们要使用某些C API,只能通过使用FontHandle类型。因此,就需要把Font类型显式转换为FontHandle类型,于是定义一个显式转换的函数get()。
1class Font{ 2public: 3 explicit Font(FontHandle fh): f(fh) // C只能使用值传递 4 { 5 // 构造时获取资源 6 } 7 FontHandle get() const {return f;} // 显示转换函数 8 ~Font(){ 9 releaseFont(f); 10 } 11private: 12 FontHandle f; 13};
但是,上面这样的显式转换的一个缺点是每次使用都要调用get()函数,比较麻烦。如下所示:
1void changeFontSize(FontHandle f, int newSize); // C API 2Font f(getFont()); 3int newFontSize; 4// ... 5changeFontSize(f.get(), newFontSize);
另一个缺点是:既然我们写了RAII资源管理类,为什么还要每次只使用它的原始资源,这与我们希望避免资源泄漏的初衷背道而驰。因此,下面来看看隐式转换。
1class Font{ 2public: 3 explicit Font(FontHandle fh): f(fh) // C只能使用值传递 4 { 5 // 构造时获取资源 6 } 7 operator FontHandle() const { // 隐式转换函数 8 return f; 9 } 10 ~Font(){ 11 releaseFont(f); 12 } 13private: 14 FontHandle f; 15};
隐式转换会使得客户调用C API时会方便很多。如下所示:
1Font f(getFont()); 2int newFontSize; 3// ... 4changeFontSize(f, newFontSize); // 隐式转换
但是,隐式转换也会增加发生错误的机会,某些类型错误就不会被编译器探测到。原本希望把一个Font对象拷贝进另一个Font对象,但如果某个人误把打Font打成了FontHandle,这样就把我们的资源管理对象变成了原始资源对象,编译器也不会报错。
1Font f1(getFont()); 2 3// ... 4FontHandle f2 = f1;
结果:程序有一个FontHandle对象被f1封装和管理,但这个FontHandle通过上面的操作被拷贝进了f2,这样f1和f2同时控制着一个FontHandle。如果f1被释放,这个FontHandle也将被释放,就会导致f2的字体被损坏。
对于使用隐式转换还是显式转换要根据不同的需求来决定,如果想保证代码的正确性,显式转换的get()函数可能是更好的选择;如果想代码自然易懂,隐式转换可能更好,两者各有优缺点。
4.总结
(1) API通常需要使用原始资源作为参数,因此我们的RAII资源管理类要保证它所封装的资源是对外界可接触的。(2) 对原始资源的访问可以通过显式转换或隐式转换。一般来说,显式转换比较安全,但隐式转换对客户使用比较方便。