0%

[Effective C++ 笔记]条款 15. 在资源管理类中提供对原始资源的访问

简介

  • API 往往要求会要求访问原始资源 (raw resources),所以没一个 RAII 类都需要提供一个可以让使用者访问原始资源的方法
  • 对原始资源的访问可以通过显式或隐式转换获得。通常来说,显示转换比较安全,而隐式转换对使用者而言比较方便。

引子

虽然资源管理类(resource-managing classes)可以很好地为我们管理资源,并且让我们免于担心资源泄漏的问题。但不可否认的是,在实际进行程序设计时,有大量的 API 都需要直接操作,除非我们永远不用这些 API,否则我们不得不绕过资源管理对象来直接访问资源。

例如,在条款 13 中,我们推荐使用智能指针(例如 auto_ptrtrl::shared_ptr 以及 C++11 之后的 std::unique_ptrstd::shared_ptr)来保存工厂函数例如 createInvestment 的 调用结果,如下所示:

1
std::shared_ptr<Investment> pInv(createInvestment());   // 见条款 13

但我们现在现在需要使用以下函数来处理 Investment 对象,如下:

1
int daysHeld(const Investment* pi);     // 返回投资天数

如果我们想直接将 pInv 传给 daysHeld 作为参数,程序会编译失败并报如下错误(完整代码见 ex1):

1
int days = daysHeld(pInv);
1
main.cpp:16:25: error: cannot convert ‘std::shared_ptr<Investment>’ to ‘Investment*’

可以发现,智能指针并不能直接转换成裸指针(反之亦然),因此我们需要一些方法来将 RAII class 对象转换为其所管理的原始资源。

将资源管理类转换为其所管理的原始资源

通过显式转换访问原始资源

通常智能指针都会提供一个 get() 成员函数来执行显式转换,即返回智能指针的内部的原始指针,用法如下:

1
int days = daysHeld(pInv.get());

通过隐式转换访问原始资源

这里的隐式转换包括两种含义:

  • 像使用原始指针一样使用智能指针

例如 std::shared_ptrstd::unique_ptr 都重载了指针取值(pointer dereference)操作符,例如 operator->operator*,使用这两个操作符时会将智能指针隐式转换为原始指针,使得我们可以像使用原始指针一样使用智能指针,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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 中,字体是一种原生数据结构):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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() 函数显式返回原始资源:

1
2
3
4
5
6
class Font {
public:
// 显式转换函数
FontHandle get() const { return f; }
...
};

但这需要在每一次调用 C API 时都需要调用这个函数,这很大程度上反而会打击程序员使用 Font 的积极性,因此可能会起反效果。因此,我们还可以令 Font 提供隐式转换函数转换为 FontHandle,如下所示:

1
2
3
4
5
6
class Font {
public:
// 隐式转换函数
operator FontHandle() const { return f; }
...
};

这样在调用 C API 时可以很自然地使用 Font,显式转换和隐式转换的用法如下:(完整代码见 ex2

1
2
3
4
5
6
7
8
Font f(getFont());
int newFontSize = 10;

// 显式转换
changeFontSize(f.get(), newFontSize);

// 隐式转换
changeFontSize(f, newFontSize);

添加输出后运行结果如下:

1
2
3
4
Got a font handle
Font size changed to 10.
Font size changed to 10.
Font released.

当然使用隐式转换的缺点也很显然,假如我们错误地使用 FontHandle 而非 Font,如下所示:

1
FontHandle f1 = f;

这时候我们意外地获取 f 的原始资源(通过隐式转换),当 f 被销毁时,f1 会变成悬空的(dangle),本来我们不应该让这种代码编译通过,但由于我们提供了隐式转换函数因此可以通过。这是我们提供隐式转换函数时需要注意的。

显式和隐式转换的对比

在我们设计 RAII 类的时候,提供显式转换函数或者隐式转换函数需要视情况而定。大致情况下显式转换函数 get() 由于其安全性比较受欢迎,但是隐式转换函数带来使用的便利也有其优点。

结论

  • API 往往要求会要吃访问原始资源 (raw resources),所以没一个 RAII 类都需要提供一个可以让使用者访问原始资源的方法
  • 对原始资源的访问可以通过显式或隐式转换获得。通常来说,显示转换比较安全,而隐式转换对使用者而言比较方便。

  • 完整可运行代码地址:Effective-Cpp-Reading-Note

  • Efftive C++ 阅读笔记:Effective C++