Effective C++: 在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes
views
| comments
这是 Effective C++ 的第 15 个条款,Provide access to raw resources in resource-managing classes,中文翻译为:在资源管理类中提供对原始资源的访问。
这个条款解决了一个非常实际的问题:我们已经接受了 RAII 是管理资源的最佳方式,但我们所写的代码需要和大量已有的、只认“原始资源”的 API 交互。例如:
std::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment* pi); // 一个只接受裸指针的 API
daysHeld(pInv); // 编译错误!类型不匹配,pInv 不是 Investment*cppRAII 对象(pInv)封装了并管理着原始资源( Investment* 指针),但它本身并不是那个原始资源。因此,RAII 类必须提供一个“出口”,允许外界在需要时能够访问到它内部封装的那个原始资源。在书中,作者提到了两种常见的做法:
- 显式转换:提供一个专门的成员函数(通常命名为 get),让用户明确地调用它来获取原始资源。
std::shared_ptr和std::unique_ptr都提供了get()成员函数。对于我们自定义的
cppdaysHeld(pInv.get()); // 正确!调用 get() 获取裸指针Font类,也可以添加一个get()方法:
cppclass Font { public: FontHandle get() const { return f; } // 显式转换函数 // ... private: FontHandle f; }; Font myFont(getFont()); changeFontSize(myFont.get(), newSize); // 用户需要明确调用 .get()- 优点:代码意图非常明确。任何看到
.get()的人都知道这里正在从一个管理类中取出其原始资源。这降低了误用的风险。 - 缺点:用户必须时刻记着在需要原始资源的地方调用
.get()。
- 优点:代码意图非常明确。任何看到
- 隐式转换:通过 C++ 的类型转换运算符,可以让类对象可以自动地、不经指明地转换为原始资源类型。实现的方法是在类中定义一个类型转换运算符
operator T(),其中T是你希望转换的目标类型。
cppclass Font { public: operator FontHandle() const { return f; } // 隐式转换函数 // ... }; Font myFont(getFont()); changeFontSize(myFont, newSize); // 非常自然!myFont 自动转换为 FontHandle- 优点:用户代码写起来非常自然流畅,就像直接使用原始资源一样。
- 缺点:方便是有代价的。隐式转换可能在你不期望的时候发生,导致难以发现的 bug。书中举了一个绝佳的例子:
程序员的意图可能是想创建一个
cppFont f1(getFont()); FontHandle f2 = f1;Font的副本,但这里实际上触发了operator FontHandle(),创建了一个原始句柄f2。当f1被销毁时,它会释放字体资源,而f2此时就变成了一个悬空句柄,再使用它就会导致程序崩溃或未定义行为。
智能指针的operator*和operator->也是一种隐式转换,但它们被设计得相对安全,因为它们返回的是对象本身或其成员的引用/指针,而不是资源句柄的副本。
总的来说,安全性通常比便利性更重要,因此:
- 首选显式转换 (
get方法)。它让代码的意图更加清晰,并且可以有效避免由意想不到的类型转换所引起的严重错误。 - 谨慎使用隐式转换。虽然它能让客户代码更漂亮,但它带来的风险(如上述的悬空句柄问题)往往得不偿失。只有在确定这种转换在所有情况下都是安全且符合直觉时,才应该考虑提供它。