对RAII的思考
Some random thoughts about RAII
写程序时常常需要申请系统资源,比如打开文件,申请一块内存。申请到这些资源后,在程序退出或者资源使用完毕后,应当正确的释放。如果不能正确释放,会造成一系列问题。比如申请的内存没有释放造成内存泄露Memory Leak,申请的进程锁没有被解锁Unlock,造成进程间的死锁DeadLock。在C++语言里,解决这类资源管理问题的管用手法是RAII (Resource Acuiquistion Is Initialization)
这篇笔记是对RAII的一点思考。
1. 什么是RAII
简单来讲,把获取资源的代码放到类的构造函数里,把释放资源的代码放到析构函数里。比如用ofstream file(“output.txt”) 可以打开文件,当file变量不起作用是,文件会被自动关闭。
比如下面这张图(from:The RAII Programming Idiom),看看这里面有多少地方需要写释放资源的代码。如果使用RAII,这些其实地方都不用留代码。
2. RAII的优缺点
RAII的好处是利用C++语言优势安全、正确的管理资源。同时RAII是C++建议的资源获取方式,这种代码可以被广大C++用户理解。
不方便之处是,使用RAII有一些陷阱。比如不要用RAII一次获取多个资源。
3.为什么C++有RAII
C++语言保证了一个类构造之后,析构函数会被自动调用。这个使用方式与资源管理的方式相似。因此可以用类的生命周期来管理资源。
4. 为什么C/Java/Python没有RAII
C语言没有原生的构造和析构函数,获取的资源不能有任何自动机制来释放。
Java/Python有语言中的支持,即Dispose Pattern。举例来说就是 try…catch..finally语句。使用者只要把释放资源的语句写到finally,资源就会被释放。
4. RAII 和Exception的关系
RAII和Exception紧密相关,更确切的说,构造函数和异常这两个特性在某种程度上互相依赖。
对于构造函数来说(获取资源的语句在构造函数里),构造函数没有返回值,因此想知道资源是否成功获取是不能从函数返回值来判断的,唯一可以用的手法是在资源获取失败时抛出异常。也就是说构造函数需要使用异常。
另一方面,使用异常之后,需要用构造函数来管理资源。因为异常抛出以后,很可能处理异常的代码和异常发生的代码不在一个层次(异常在Call Stack上逐层向上)。为了实现异常安全(Exception Safe),应该使用构造函数(另一个选择是智能指针,但智能指针有智能指针的问题,详见C++FAQ的讨论)。
对已有的C++代码来说,实现或检查代码是不是异常安全不是一个的简单人物。这种情况下,异常这个特性往往会被禁用(比如Google C++ style guide)。如果异常被禁用了,我们就没法从构造函数本身获知资源是否成功获取,那是不是说我们没法使用RAII特性呢?
答案是否定的。我们可以在获取资源后,用其他的类函数来检查资源获取是否成功。比如ostream::is_open()就可以检查文件是否被正常打开。
5. 怎么绕开RAII
在C等不提供RAII支持的语言里,可以直接绕开RAII,即保证获取资源后,程序的每一个出口都有释放资源的语句。
这种方法有可能造成多处重复的资源释放代码,或者使用goto语句把所有程序跳转到一处资源释放代码。
6. 实践中怎么用RAII
实践中除了把资源获取的语句写到构造函数,把资源释放的语句写到析构函数,还应当注意:
1)获取多个资源时,可以写在多个类的构造函数里,使得每一个类的构造函数对应一个资源。这样在任何资源获取失败时,已经获得的资源会得到释放
2)有时候获取资源失败等于程序失败(Fatal Condition),这种情况下可以直接退出(exit),把清理资源的任务留给操作系统。