简介
- API 往往要求会要求访问原始资源 (raw resources),所以没一个 RAII 类都需要提供一个可以让使用者访问原始资源的方法
- 对原始资源的访问可以通过显式或隐式转换获得。通常来说,显示转换比较安全,而隐式转换对使用者而言比较方便。
引子
虽然资源管理类(resource-managing classes)可以很好地为我们管理资源,并且让我们免于担心资源泄漏的问题。但不可否认的是,在实际进行程序设计时,有大量的 API 都需要直接操作,除非我们永远不用这些 API,否则我们不得不绕过资源管理对象来直接访问资源。
例如,在条款 13 中,我们推荐使用智能指针(例如 auto_ptr
、trl::shared_ptr
以及 C++11 之后的 std::unique_ptr
和 std::shared_ptr
)来保存工厂函数例如 createInvestment
的 调用结果,如下所示:
std::shared_ptr<Investment> pInv(createInvestment()); // 见条款 13
但我们现在现在需要使用以下函数来处理 Investment
对象,如下:
int daysHeld(const Investment* pi); // 返回投资天数
如果我们想直接将 pInv
传给 daysHeld
作为参数,程序会编译失败并报如下错误(完整代码见 ex1):
int days = daysHeld(pInv);
main.cpp:16:25: error: cannot convert ‘std::shared_ptr<Investment>’ to ‘Investment*’
可以发现,智能指针并不能直接转换成裸指针(反之亦然),因此我们需要一些方法来将 RAII class 对象转换为其所管理的原始资源。
将资源管理类转换为其所管理的原始资源
通过显式转换访问原始资源
通常智能指针都会提供一个 get()
成员函数来执行显式转换,即返回智能指针的内部的原始指针,用法如下:
int days = daysHeld(pInv.get());
通过隐式转换访问原始资源
这里的隐式转换包括两种含义:
- 像使用原始指针一样使用智能指针
例如 std::shared_ptr
和 std::unique_ptr
都重载了指针取值(pointer dereference)操作符,例如 operator->
和 operator*
,使用这两个操作符时会将智能指针隐式转换为原始指针,使得我们可以像使用原始指针一样使用智能指针,如下所示:
class Investment { // investment 继承体系的根类
public:
bool isTaxFree() cosnt;
};
Investment* createInvestment(); // 工厂函数
std::shared_ptr<Investment> pi1(createInvestment()); // 使用 std::shared_ptr 管理资源
bool taxable1 = !(pi1->isTaxFree()); // 通过 operator-> 访问资源
std::unique_ptr<Investment> pi2(createInvestment()); // 使用 std::shared_ptr 管理资源
bool taxable2 = !((*pi1).isTaxFree()); // 通过 operator* 访问资源
- 直接访问原始指针
有时候我们还是会需要直接使用原始指针,考虑以下例子:这是有关于字体的 RAII 类(在 C API 中,字体是一种原生数据结构):
// C API to get a font handle
FontHandle getFont();
// Another C API used to release font
void releaseFont(FontHandle fh);
// RAII class
class Font {
public:
explicit Font(FontHandle fh) : f(fh) {} // 获取资源
~Font() { releaseFont(f); } // 释放资源
private:
FontHandle f; // 原始字体资源
};
考虑到我们在使用 C API 的时候会有大量的函数需要直接处理 FontHandle
,因此我们需要频繁的将 RAII 类 Font
的对象转换为 FontHandle
:例如改变字体 changeFontSize()
等等。其中一种做法是我们可以提供一个 get()
函数显式返回原始资源:
class Font {
public:
// 显式转换函数
FontHandle get() const { return f; }
...
};
但这需要在每一次调用 C API 时都需要调用这个函数,这很大程度上反而会打击程序员使用 Font
的积极性,因此可能会起反效果。因此,我们还可以令 Font
提供隐式转换函数转换为 FontHandle
,如下所示:
class Font {
public:
// 隐式转换函数
operator FontHandle() const { return f; }
...
};
这样在调用 C API 时可以很自然地使用 Font
,显式转换和隐式转换的用法如下:(完整代码见 ex2)
Font f(getFont());
int newFontSize = 10;
// 显式转换
changeFontSize(f.get(), newFontSize);
// 隐式转换
changeFontSize(f, newFontSize);
添加输出后运行结果如下:
Got a font handle
Font size changed to 10.
Font size changed to 10.
Font released.
当然使用隐式转换的缺点也很显然,假如我们错误地使用 FontHandle
而非 Font
,如下所示:
FontHandle f1 = f;
这时候我们意外地获取 f
的原始资源(通过隐式转换),当 f
被销毁时,f1
会变成悬空的(dangle),本来我们不应该让这种代码编译通过,但由于我们提供了隐式转换函数因此可以通过。这是我们提供隐式转换函数时需要注意的。
显式和隐式转换的对比
在我们设计 RAII 类的时候,提供显式转换函数或者隐式转换函数需要视情况而定。大致情况下显式转换函数 get()
由于其安全性比较受欢迎,但是隐式转换函数带来使用的便利也有其优点。
结论
-
API 往往要求会要吃访问原始资源 (raw resources),所以没一个 RAII 类都需要提供一个可以让使用者访问原始资源的方法
-
对原始资源的访问可以通过显式或隐式转换获得。通常来说,显示转换比较安全,而隐式转换对使用者而言比较方便。
-
完整可运行代码地址:Effective-Cpp-Reading-Note
-
Efftive C++ 阅读笔记:Effective C++