C++ 程式設計/RAII
RAII 技術通常用於在多執行緒應用程式中控制執行緒鎖。另一個典型的 RAII 示例是檔案操作,例如 C++ 標準庫中的檔案流。輸入檔案流在物件的建構函式中開啟,並在物件銷燬時關閉。由於 C++ 允許在 堆疊 上分配物件,因此 C++ 的作用域機制可用於控制檔案訪問。
使用 RAII,我們可以使用類解構函式來保證清理,類似於其他語言中的 finally 關鍵字。這樣做可以自動執行任務,從而避免錯誤,但也提供了不使用它的自由。
RAII 也用於(如以下示例所示)確保異常安全。RAII 使得在沒有大量使用 try/catch 塊的情況下避免資源洩漏成為可能,並在軟體行業中得到廣泛使用。
動態分配記憶體(使用new)的擁有權可以使用 RAII 控制。為此,C++ 標準庫定義了 auto ptr。此外,共享物件的生存期可以透過具有共享所有權語義的智慧指標來管理,例如 C++ 中由 Boost 庫 定義的 boost::shared_ptr 或 Loki 庫 中基於策略的 Loki::SmartPtr。
以下 RAII 類是對 C 標準庫檔案系統呼叫的輕量級包裝器。
#include <cstdio>
// exceptions
class file_error { } ;
class open_error : public file_error { } ;
class close_error : public file_error { } ;
class write_error : public file_error { } ;
class file
{
public:
file( const char* filename )
:
m_file_handle(std::fopen(filename, "w+"))
{
if( m_file_handle == NULL )
{
throw open_error() ;
}
}
~file()
{
std::fclose(m_file_handle) ;
}
void write( const char* str )
{
if( std::fputs(str, m_file_handle) == EOF )
{
throw write_error() ;
}
}
void write( const char* buffer, std::size_t num_chars )
{
if( num_chars != 0
&&
std::fwrite(buffer, num_chars, 1, m_file_handle) == 0 )
{
throw write_error() ;
}
}
private:
std::FILE* m_file_handle ;
// copy and assignment not implemented; prevent their use by
// declaring private.
file( const file & ) ;
file & operator=( const file & ) ;
} ;
此 RAII 類可以按如下方式使用
void example_with_RAII()
{
// open file (acquire resource)
file logfile("logfile.txt") ;
logfile.write("hello logfile!") ;
// continue writing to logfile.txt ...
// logfile.txt will automatically be closed because logfile's
// destructor is always called when example_with_RAII() returns or
// throws an exception.
}
如果不使用 RAII,每個使用輸出日誌的函式都必須顯式管理檔案。例如,不使用 RAII 的等效實現如下
void example_without_RAII()
{
// open file
std::FILE* file_handle = std::fopen("logfile.txt", "w+") ;
if( file_handle == NULL )
{
throw open_error() ;
}
try
{
if( std::fputs("hello logfile!", file_handle) == EOF )
{
throw write_error() ;
}
// continue writing to logfile.txt ... do not return
// prematurely, as cleanup happens at the end of this function
}
catch(...)
{
// manually close logfile.txt
std::fclose(file_handle) ;
// re-throw the exception we just caught
throw ;
}
// manually close logfile.txt
std::fclose(file_handle) ;
}
如果 fopen() 和 fclose() 可能丟擲異常,則 file 和 example_without_RAII() 的實現將變得更加複雜;但是,example_with_RAII() 不會受到影響。
RAII 習慣用法的主要思想是類 file 封裝了任何有限資源的管理,例如 FILE* 檔案控制代碼。它保證在函式退出時資源將被正確釋放。此外,file 例項保證一個有效的日誌檔案可用(如果檔案無法開啟,則丟擲異常)。
在存在異常的情況下,還有一個大問題:在 example_without_RAII() 中,如果分配了多個資源,但在它們的分配之間丟擲異常,則沒有普遍的方法可以知道在最終的 catch 塊中需要釋放哪些資源 - 並且釋放未分配的資源通常是一件壞事。RAII 可以解決這個問題;自動變數以與它們構造相反的順序銷燬,並且只有在物件完全構造後(建構函式內部沒有丟擲異常)才銷燬該物件。因此,example_without_RAII() 永遠不可能像 example_with_RAII() 那樣安全,除非對每種情況進行特殊編碼,例如檢查無效的預設值或巢狀 try-catch 塊。事實上,應該注意的是,example_without_RAII() 在本文的早期版本中包含資源錯誤。
這使得 example_with_RAII() 不必像以前那樣顯式管理資源。當多個函式使用 file 時,這將簡化和減少總體程式碼大小,並有助於確保程式正確性。
example_without_RAII() 類似於在非 RAII 語言(如 Java)中用於資源管理的習慣用法。雖然 Java 的 try-finally 塊允許正確釋放資源,但負擔仍然落在程式設計師身上,以確保正確行為,因為每個使用 file 的函式都可能顯式地要求使用 try-finally 塊銷燬日誌檔案。