About
RSS

Bit Focus


C++ 对象构造时的异常与 RAII 的救赎

Posted at 2011-06-21 13:27:55 | Updated at 2021-09-20 02:28:39

    在上一篇文章中简单介绍了一下 RAII 的原理, 举了个读文件的例子. 这个例子稍微单薄了一些, 它只封装了一个需要 RAII 机制管理的资源 (FILE*). 软件工程中流行的观念是, 不具备扩展性, 经不起追加功能的东西注定会悲剧. 现在假如需要给这货增加个缓冲区, 也许这样是可以的
struct File {
    File(char const* filename, int buffer_size)
        : file(fopen(filename, "r"))
        , buffer(new char[buffer_size])
    {
        if (NULL == file) {
            throw std::runtime_error(std::string("fail to open ") + filename);
        }
    }

    ~File()
    {
        fclose(file);
        delete[] buffer;
    }
private:
    FILE* file;
    char* buffer;

/* other members */
};
    在 buffer 分配失败时, 一般会抛出 std::bad_alloc.
    这个类型的破绽相当多, 稍不注意就有可能漏资源. 首先是刚刚提到的 buffer 分配失败抛异常, 那么假如这个时候 file 已经打开成功了, 它会被关闭么? 其次, 假设 buffer 成功分配, 但这时 file 打开失败, 那么 buffer 是否会被释放呢?
    很不幸的, 两者的答案都是. 还是那句话, 因为 File 的构造函数没有走完, 这时抛出异常, 那么析构函数不会被执行. 因此, 不要尝试构造控制多于一个资源的类型. 而遇到这种需求, 应该拆分资源, 然后将这些单元类型进行聚合, 如
struct File {
    explicit File(char const* filename)
        : file(fopen(filename, "r"))
    {
        if (NULL == file) {
            throw std::runtime_error(std::string("fail to open ") + filename);
        }
    }

    ~File()
    {
        fclose(file);
    }
private:
    FILE* file;

/* other members */
};

struct Buffer {
    explicit Buffer(int buffer_size)
        : buffer(new char[buffer_size])
    {}

    ~Buffer()
    {
        delete[] buffer;
    }
private:
    char* buffer;

/* other members */
};

struct BufferedFile {
    BufferedFile(char const* filename, int buffer_size)
        : file(filename)
        , buffer(buffer_size)
    {}

    File file;
    Buffer buffer;

/* other members */
};
    当然这里的 Buffer 类型又犯了 C++ 中的另一个忌, STL 里有 std::stringstd::vector 呢, 这又造起了轮子哎.

    这样写很明显地能解决: file 先行构造, 打开失败的话, 自然 buffer 就没机会构造了. 而当 buffer 构造失败的时候会发生什么呢? 当然, file 会被析构, 要不然 RAII 连这么简单的情形都对付不了, 那就坑爹坑到死了.
    看待初始化列表里面那一堆成员 filebuffer 的构造, 其实可以想象成在 BufferedFile 构造函数开头处声明的栈中对象, 其中任何一个构造时抛出异常时, 之前已经完成构造的对象则会依次析构掉. 不过, 有一点跟普通的栈中对象不同的是, 在 BufferedFile 构造函数结束之后, 也就是上篇文章中提到的对象从不可用状态切换到可用状态这个时刻之后, 它们不会析构. 这一点也是很显然的, 它们此时作为成员, 将与持有它们的 BufferedFile 对象拥有相同的作用域. 这又是 RAII 为程序员提供的状态切换的透明性的另一个方面.

Post tags:   RAII  Exception Handling  C++

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site