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
掉复制构造函数, 并加上转移构造函数哦.