About
RSS

Bit Focus


从 Python 的 with 到 RAII

Posted at 2011-06-20 08:43:07 | Updated at 2021-09-20 02:45:07

    在上一篇文章中提到了 Python 的 with 结构, 其背后的思想正是 RAII. 在 C++ 中, RAII 的样子看起来跟 Python 中的会非常不一样, 还是以打开读取文件为例子
#include <cstdio>
#include <stdexcept>
#include <iostream>

struct File {
    explicit File(char const* filename)
        : f(fopen(filename, "r"))
    {
        if (NULL == f) {
            throw std::runtime_error(std::string("fail to open ") + filename);
        }
    }

    std::string readLine()
    {
        char buffer[256] = { 0 }; // just for demo
        if (NULL == fgets(buffer, 256, f)) {
            if (!feof(f)) {
                throw std::runtime_error("error occurs while reading file");
            }
            throw std::out_of_range("end of file")
        }
        return buffer;
    }

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

int main()
{
    try {
        File file("litsu");
        while (true) {
            std::cout << file.readLine();
        }
    } catch (std::runtime_error e) {
        std::cerr << e.what() << std::endl;
    } catch (std::out_of_range) {}
    return 0;
}
    注意看 try 块中间的语句, 看起来流程很清晰, 完全不像 Python 里面那样, 还要弄个 with 块, 多一级缩进. 都说 C++ 代码没 Python 的精神, 然而在这种小地方 C++ 反而超过了 Python 呢.

    其实, Python 的 with 块更像个语法糖, 上篇文章中有提到, 双层 try 在 Python 中能等效地解决这个问题, 只是看起来丑很多. 这个问题的根本在于, Python 这样不具备 RAII 特性的语言没能处理好对象从不可用状态切换到可用状态的状态过渡. 回顾一下双层 try 结构的处理方式
def readFile():
    try:
        f = open('sawako', 'r')
        pass
        try:
            process(f.readlines())
        except:
            print 'error occurs while reading file'
        finally:
            f.close()
    except:
        print 'error occurs while reading file'
    在 pass 那一行就是这个状态切换点. 在这一点之前发生异常, 程序不需要走到 f.close() 这一句, 而在这一点之后发生异常, 就需要执行文件关闭. 这本来就是以时间轴为根基的顺序程序设计思路的硬伤, 因此而把程序搞得乱糟糟有的时候也是别无它法的.

    RAII 巧妙地解决这个问题. 可谓大道至简, C++ 中以对象方式实现的 RAII 的核心思想可以用一句话概括
这句话有些需要补充的细节, 比如析构的顺序, 以及析构函数不能抛出异常的限制, 暂时这些先不谈这些. 析构函数想必大家都有了解, 这里就不多嘴了. 作用域这个概念比较重要, 以上面的例子来说
    try {
        File file("litsu");
        while (true) {
            std::cout << file.readLine();
        }
    } catch (std::runtime_error e) {
        std::cerr << e.what() << std::endl;
    } catch (std::out_of_range) {}
    file 的作用域从 while 这句开始, 到 try 块结束, 也就是在那些地方 file 对象够能被合法引用. 如果在 file 作用域之前发生异常, 比如这样 (代码很挫, 仅作为演示)
    try {
        throw std::runtime_error("tea time!");
        File file("litsu");
        while (true) {
            std::cout << file.readLine();
        }
    }
那么 file 不会执行析构.
    而类似下面的临时变量使用
void call(std::string const&, std::string const&);
/* ... */
call(std::string("nodoka"), "ui");
两个 std::string 对象的作用域就是这一句函数调用. 在函数调用退出之后, 这两个对象将被依次析构.

    解开问题的关键在于成功构造这个条件. 当构造时当场抛出异常, 比如上面代码中文件打开失败得到 NULL 的时候会抛出异常, 这时 file 还未被成功构造, 因此不会执行析构. 只有构造函数的流程结束, 也就是上面所说的对象从不可用状态切换到可用状态这个临界点过了, 在作用域退出时才会执行析构.
    而在刚才 call(std::string("nodoka"), "ui") 这个例子中, 有两个 std::string 对象需要构造, 假设构造第一个成功了, 而构造第二个失败并抛出异常, 那么第一个将会被析构 (这里的第 x 个并不表示参数顺序, 实际上 C++ 标准不规定参数的构造顺序, 而通常实现是逆于参数顺序构造的).

    也就是说, C++ 在运行期记录各个对象的构造情况, 因此能够在 stack unwind 阶段准确地析构特定的对象, 从而让对象从不可用状态切换到可用状态这个过渡对程序员透明. 而对象析构的顺序则逆于对象的构造顺序.

    析构函数抛出异常, 或者说资源释放时的异常是一个雷区. 原则上, 资源释放时是不可能出现异常的, 在实践中有这种问题也是极其稀有的. (这一点也可以参考一下 Java 中 java.lang.Object.finalize 的文档: "If an uncaught exception is thrown by the finalize method, the exception is ignored and finalization of that object terminates.") 只要不做傻事这应该没问题的.

    在 C++ 2011 标准出台之前, RAII 可谓几成而败, 因为它没能处理好资源控制权转移. 在 C++ move semantic 推出之后, C++ 的 RAII 机制才算完备了. 上面的 File 类如果要用于生产, 按照 move semantic 的规程, 还得 delete 掉复制构造函数, 并加上转移构造函数哦.

Post tags:   Exception Handling  C++  RAII  Python

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