Effective C++条款15——在资源管理类中提供对原始资源的访问(资源管理)

资源管理类(resource-managing classes)很棒。它们是你对抗资源泄漏的堡垒。排除此等泄漏是良好设计系统的根本性质。在一个完美世界中你将倚赖这样的classes来处理和资源之间的所有互动,而不是玷污双手直接处理原始资源(rawresources)。但这个世界并不完美。许多APIs直接指涉资源,所以除非你发誓(这其实是一种少有实际价值的举动)永不录用这样的APIs,否则只得绕过资源管理对象(resource-managing objects)直接访问原始资源(raw resources)。

举个例子,条款13 导入一个观念:使用智能指针如auto ptr或tr1 : :shared ptr保存factory函数如createInvestment的调用结果:

std::tr1::shared_ptr pInv(createInvestment());

假设你希望以某个函数处理Investment对象,像这样:

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

你想要这么调用它:

int days = daysHeld(pInv);        // 错误

却通不过编译,因为daysHeld需要的是Investment*指针,你传给它的却是个类型为tr1::shared_ ptr的对象。

这时候你需要一个函数可将RAII class对象(本例为tr1:: shared ptr)转换为其所内含之原始资源(本例为底部之Investment*)。有两个做法可以达成目标:显式转换和隐式转换。

tr1::shared_ptr和 auto_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件):

int days = daysHeld(Pinv.get());        // 很好,将pInv内的原始指针传给daysHeld

就像(几乎)所有智能指针一样,tr1::shared ptr和 auto_ptr也重载了指针取值(pointer dereferencing)操作符(operator->和operator*),它们允许隐式转换至底部原始指针:

class Investment {                // investment继承体系的根类
public:
    bool isTaxFree() const;
    // ...
};

Investment* createInvestment();            // factory函数
std::tr1::shared_ptr pi1(createInvestment);    // 令tr1::shared_ptr管理一笔资源
bool taxable1 = !(pi1->isTaxFree());                       // 经由operator->访问资源
// ...
std::auto_ptr pi2(createInvestment());         // 令auto_ptr管理一笔资源

bool taxable2 = !((*pi2).isTaxFree());                     // 经由operator*访问资源
// ...

由于有时候还是必须取得 RAII对象内的原始资源,某些RAII class设计者于是联想到“将油脂涂在滑轨上”,做法是提供一个隐式转换函数。考虑下面这个用于字体的RAlI class(对 C API而言字体是一种原生数据结构):

FontHandle getFont();                // 这是个C API,简化参数
void releaseFont(FontHandle fh);     // 来自同一组C API

class Font {
public:
    explicit Font(FontHandle fh)     // 获得资源
        :f(fh)                       // pass by value
    {}

    ~Font() { releaseFont(f); }      // 释放资源

private:
    FontHandle f;                    // 原始数据
};

假设有大量与字体相关的CAPI,它们处理的是FontHandles,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。Font class可为此提供一个显式转换函数,像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);

某些程序员可能会认为,如此这般地到处要求显式转换,足以使人们倒尽胃口,不再愿意使用这个class,从而增加了泄漏字体的可能性,而Font class 的主要设计目的就是为了防止资源(字体)泄漏。

另一个办法是令Font提供隐式转换函数,转型为FontHandle:

class Font {
public:
    // ...
    operator FontHandle() const        // 隐式转换函数
    { return f; }
    // ...
};

这使得客户调用CAPI时比较轻松且自然:

Font f(getFont());
int newFontSize;
// ...
changeFontSize(f, newFontSize);            // 将Font隐式转换为FontHandle

但是这个隐式转换会增加错误发生机会。例如客户可能会在需要Font时意外创建一个FontHandle:
 

Font f1(getFont());
// ...
FontHandle f2 = f1;

以上程序有个FontHandle由 Font对象f1管理,但那个FontHandle也可通过直接使用f2取得。那几乎不会有好下场。例如当f1被销毁,字体被释放,而f2因此成为“虚吊的”( dangle) 。

是否该提供一个显式转换函数(例如get成员函数)将RAII class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAIl class被设计执行的特定工作,以及它被使用的情况。最佳设计很可能是坚持条款18的忠告:“让接口容易被正确使用,不易被误用”。通常显式转换函数如 get是比较受欢迎的路子,因为它将“非故意之类型转换”的可能性最小化了。然而有时候,隐式类型转换所带来的“自然用法”也会引发天秤倾斜。

你的内心也可能认为,RAll class内的那个返回原始资源的函数,与“封装”发生矛盾。那是真的,但一般而言它谈不上是什么设计灾难。RAII classes并不是为了封装某物而存在;它们的存在是为了确保一个特殊行为——资源释放——会发生。如果一定要,当然也可以在这基本功能之上再加一层资源封装,但那并非必要。此外也有某些RAII classes结合十分松散的底层资源封装,藉以获得真正的封装实现。例如tr1 : :shared ptr将它的所有引用计数机构封装了起来,但还是让外界很容易访问其所内含的原始指针。就像多数设计良好的classes一样,它隐藏了客户不需要看的部分,但备妥客户需要的所有东西。

请记住

APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。

对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

你可能感兴趣的:(Effective,C++,c++,开发语言,keep,studying,学习,Effective,C++)