RAII
RAII,全稱資源取得即初始化(英語:Resource Acquisition Is Initialization),它是在一些物件導向語言中的一種慣用法。RAII源於C++,在Java,C#,D,Ada,Vala和Rust中也有應用。1984-1989年期間,比雅尼·斯特勞斯特魯普和安德魯·柯尼希在設計C++異常時,為解決資源管理時的異常安全性而使用了該用法[1],後來比雅尼·斯特勞斯特魯普將其稱為RAII[2]。
RAII要求,資源的有效期與持有資源的對象的生命期嚴格繫結,即由對象的建構函式完成資源的分配(取得),同時由解構函式完成資源的釋放。在這種要求下,只要對象能正確地解構,就不會出現資源泄漏問題。
作用
RAII的主要作用是在不失代碼簡潔性[3]的同時,可以很好地保證代碼的異常安全性。
下面的C++實例說明了如何用RAII訪問檔案和互斥量:
#include <string>
#include <mutex>
#include <iostream>
#include <fstream>
#include <stdexcept>
void write_to_file(const std::string & message)
{
// 创建关于文件的互斥锁
static std::mutex mutex;
// 在访问文件前进行加锁
std::lock_guard<std::mutex> lock(mutex);
// 尝试打开文件
std::ofstream file("example.txt");
if (!file.is_open())
throw std::runtime_error("unable to open file");
// 输出文件内容
file << message << std::endl;
// 当离开作用域时,文件句柄会被首先析构 (不管是否抛出了异常)
// 互斥锁也会被析构 (同样地,不管是否抛出了异常)
}
C++保證了所有棧對象在生命周期結束時會被銷毀(即呼叫解構函式)[4],所以該代碼是異常安全的。無論在write_to_file函數正常返回時,還是在途中投擲異常時,都會引發write_to_file函數的堆疊回退,而此時會自動呼叫lock和file對象的解構函式。
當一個函數需要通過多個局部變數來管理資源時,RAII就顯得非常好用。因為只有被構造成功(建構函式沒有投擲異常)的對象才會在返回時呼叫解構函式[4],同時解構函式的呼叫順序恰好是它們構造順序的反序[5],這樣既可以保證多個資源(對象)的正確釋放,又能滿足多個資源之間的依賴關係。
由於RAII可以極大地簡化資源管理,並有效地保證程式的正確和代碼的簡潔,所以通常會強烈建議在C++中使用它。
典型用法
RAII在C++中的應用非常廣泛,如C++標準庫中的lock_guard[6]便是用RAII方式來控制互斥量:
template <class Mutex> class lock_guard {
private:
Mutex& mutex_;
public:
lock_guard(Mutex& mutex) : mutex_(mutex) { mutex_.lock(); }
~lock_guard() { mutex_.unlock(); }
lock_guard(lock_guard const&) = delete;
lock_guard& operator=(lock_guard const&) = delete;
};
程式設計師可以非常方便地使用lock_guard,而不用擔心異常安全問題
extern void unsafe_code(); // 可能抛出异常
using std::mutex;
using std::lock_guard;
mutex g_mutex;
void access_critical_section()
{
lock_guard<mutex> lock(g_mutex);
unsafe_code();
}
RRID
RAII還有另外一種被稱為RRID(Resource Release Is Destruction)的特殊用法[7],即在構造時沒有「取得」資源,但在解構時釋放資源。ScopeGuard[8]和Boost.ScopeExit[9]就是RRID的典型應用:
#include <functional>
class ScopeGuard {
private:
typedef std::function<void()> destructor_type;
destructor_type destructor_;
bool dismissed_;
public:
ScopeGuard(destructor_type destructor) : destructor_(destructor), dismissed_(false) {}
~ScopeGuard()
{
if (!dismissed_) {
destructor_();
}
}
void dismiss() { dismissed_ = true; }
ScopeGuard(ScopeGuard const&) = delete;
ScopeGuard& operator=(ScopeGuard const&) = delete;
};
ScopeGuard通常用於省去一些不必要的RAII封裝,例如
void foo()
{
auto fp = fopen("/path/to/file", "w");
ScopeGuard fp_guard([&fp]() { fclose(fp); });
write_to_file(fp); // 异常安全
}
在D語言中,scope關鍵字也是典型的RRID用法,例如
void access_critical_section()
{
Mutex m = new Mutex;
lock(m);
scope(exit) unlock(m);
unsafe_code(); // 异常安全
}
Resource create()
{
Resource r = new Resource();
scope(failure) close(f);
preprocess(r); // 抛出异常时会自动调用close(r)
return r;
}
與finally的比較
雖然RAII和finally都能保證資源管理時的異常安全,但相對來說,使用RAII的代碼相對更加簡潔。 如比雅尼·斯特勞斯特魯普所說,「在真實環境中,呼叫資源釋放代碼的次數遠多於資源類型的個數,所以相對於使用finally來說,使用RAII能減少代碼量。」[10]
例如在Java中使用finally來管理Socket資源
void foo() {
Socket socket;
try {
socket = new Socket();
access(socket);
} finally {
socket.close();
}
}
在採用RAII後,代碼可以簡化為
void foo() {
try (Socket socket = new Socket()) {
access(socket);
}
}
特別是當大量使用Socket時,重複的finally就顯得沒有必要。
參考資料
- Stroustrup, Bjarne. The C++ Programming Language [C++程式語言]. Addison-Wesley. 2000 [2014-09-29]. ISBN 0-201-70073-5. (原始內容存檔於2014-06-30) (英語).
- Stroustrup, Bjarne. The Design and Evolution of C++ [C++語言的設計和演化]. Addison-Wesley. 1994 [2014-09-29]. ISBN 0-201-54330-3. (原始內容存檔於2023-06-06) (英語).
- Wilson, Matthew. ,Imperfect C++ [Imperfect C++中文版]. Addison-Wesley. 2004 [2023-11-10]. ISBN 0321228774. (原始內容存檔於2023-06-05) (英語).
- ^ Exception Handling for C++ (頁面存檔備份,存於互聯網檔案館), 5 Handling of Destructors
- ^ Stroustrup 1994,chpt. 16.5 Resource Management. I called this technique 「resource acquisition is initialization.」
- ^ C++ FAQ, "I have too many try blocks; what can I do about it?" (頁面存檔備份,存於互聯網檔案館)
- ^ 4.0 4.1 Stroustrup 2000,chpt. 14.4.1 Using Constructors and Destructors.
- ^ C++ FAQ, "What's the order that local objects are destructed?" (頁面存檔備份,存於互聯網檔案館)
- ^ lock_guard(頁面存檔備份,存於互聯網檔案館)
- ^ Wilson 2004,chpt. 3.4 RRID.
- ^ Andrei Alexandrescu, Change the Way You Write Exception-Safe Code (頁面存檔備份,存於互聯網檔案館)
- ^ Boost.ScopeExit(頁面存檔備份,存於互聯網檔案館)
- ^ Bjarne Stroustrup's C++ Style and Technique FAQ (頁面存檔備份,存於互聯網檔案館). "Why doesn't C++ provide a "finally" construct?" (頁面存檔備份,存於互聯網檔案館)