如何評價 RAII 特性在 C++ 中的大範圍運用?

類似的行為(靠對象的作用域/生存期來管理資源)在很多語言中也都存在,如 Python 的 context manager。但是常用的編程語言中好像沒有哪種能夠像 C++ 這樣把 RAII 做到近乎欽定的地位。如何評價這一現象?


因為RAII確實好用,既簡潔,又健壯,又強大。論簡潔,RAII類定義好之後,用的時候定義類對象即可。回收資源不需要再寫任何一個字。論健壯,資源回收保證執行,無論你增減return還是扔異常都不會出問題。論強大,RAII不僅能管理內存,還能管理任何需要回收的資源。互斥鎖,資料庫連接,文件句柄,哪個都沒問題,而且寫法一致。

一種語言就是一種表達習慣,C++管理資源的時候自然形成的最佳表達方式就是RAII。你覺得「欽定」,其實是業界的建議喊得響。語法層面上並沒任何限制強迫你使用RAII。

另外RAII也不是唯一選項了。如果你只需要確保資源正確釋放而不需要其他管理功能,也可以使用簡化版的RRID,實現一個scope guard然後扔一個lambda去釋放資源。申請資源的時候需要多啰嗦一句,但是RAII類都不需要寫了。


不明白題主所說的欽定是什麼意思。按題乾的意思的話,我認為可能是指

沒有專門或者明顯的語法來表示RAII。

如果是這個理解,那麼我只能說,這就對了。因為RAII的目標,就是要讓變數控制的資源的生存期嚴格等同於變數自身的生存期,而變數的生存期已經由語法里作用域部分規定了,所以不應該需要專門的語法來進行說明。

為什麼C++能做到?我認為是值語意的廣泛使用。在值語意的環境下,變數獨享其控制的資源,所以,只需要說清楚

  • 變數作用域結束時自動調用析構函數

  • 變數的複製/移動構造函數

這兩個事情,作用域就可以直接用來控制資源的生存期,這正好滿足了RAII的要求,所以不再需要額外的語法來說明這個事情。

而在引用語意的環境下,變數不再獨享資源,這樣變數的作用域無法真正表示資源的生存期,所以必須要try with resource或using這樣的語法來表達。


我來說一點別的。

raii固然是一個好的特性,但是在某些c庫調用的邊界上可能會出現意想不到的行為。

舉個最近我自己因為蛋疼而踩到的坑:我想從cpp裡面調用lua的api來讀取一個配置文件,但是lua的內部採用setjmp和longjmp來模擬了異常機制。在這種情況下,c和cpp的兩種資源管理機制水火不容,難以無縫銜接。

假設說一個lua的pcall中拋出了lua異常,那麼lua棧會unwind,並且gc掉棧上的對象,此時由於已經longjmp走了,根本都無法觸及到某個作用域的結尾,raii也就沒法觸發了,這就導致了資源泄漏。

假設一個cpp調用產生了異常呢?此時即便採用raii保證了cpp對象的內存管理不出錯,lua棧的結構卻被破壞了,可能在隨後的代碼中出現某種錯亂的行為。

這也就說明了,在和c庫交互的時候,恐怕難以將raii完美整合進來。這種時候,往往需要優秀的技術骨幹主導才能保證不出錯誤。我自己因為實在不懂lua所以只能採用鴕鳥政策:放棄採用可以拋出異常的cpp方法,用new構造一個指針並轉交給lua虛擬機的gc機制,相當於把lua虛擬機當成一個unique_ptr來使用。一旦cpp真的拋出了異常,絕不catch,任憑程序崩潰,反正只是讀個配置文件,並不危及到程序核心邏輯,不如早死早超生。

正確使用lua的姿勢,恐怕只能請相關專家來普及一下了


你不用也可以,只要你有更好的辦法保證不出現mem leak


如果我說,RAII是C++的邏輯基礎,不知道有人反對不,不信來想一下沒有RAII存在的繼承,虛函數世界?

如果把這個世界分成3類,拿C/C++/Java舉例:

C:拉完屎,自己選擇如下方式擦屁股(不擦,紙,石頭,大刀....)

C++ with RAII:拉完屎,自己用紙擦屁股,您還可以選擇(不擦,石頭,大刀...)

Java with GC:拉完屎,讓別人給你擦屁股

關於GC,對於Modern C++來說,shared_ptr其實就是引用計數方式的GC模型;OC,Python其實也是使用的這種方式;

早年我還試驗過一個C++項目,採用重載new和delete的方式做GC(跟蹤處理方式),如你所願,項目最終失敗了,主要原因就是因為很多人用C++時,不是選擇用紙擦屁股,而選擇了其他方式;

貼一段7年前給公司新員工做C++培訓時寫的東西,現在請直接用:shared_ptr unique_ptr代替:

在C/C++中,存在著各種各樣的資源分配與釋放的配對, 如
malloc free
fopen fclose
CreateBitmap DeleteObject
CreateMenu DestoryMenu
CreateDC DeleteDC
……

而因為邏輯控制關係,為了更好的釋放資源,會利用到類超出生命周期的自動析構特性
所以經常實現這樣的一些簡單類
struct _DeleteObject {
HBITMAP m_hBmp;
_DeleteObject(HBITMAP hBmp) : m_hBmp(hBmp){};
~_DeleteObject() { ::DeleteObject(m_hBmp); }
};

這種東西,寫多了還是比較煩
於是想,有沒有一種簡單的方式,可以實作一個通用的類;
用三種方式實現了自己的想法,現在逐一列出,和大家共享

方式一,採用虛函數的方式:

template&
class auto_free
{
struct auto_free_base {
virtual ~auto_free_base(){};
virtual _Type get() = 0;
};
template &
class auto_free_impl : public auto_free_base {
_Type _type;
_Dtor _dtor;
public:
auto_free_impl(_Type type, _Dtor dtor) : _type(type), _dtor(dtor){}
~auto_free_impl() { _dtor(_type); }
virtual _Type get() {return _type;}
};

auto_free_base *_pi;
public:
template&
auto_free(_Type type, _Dtor dtor) {
try {
_pi = new auto_free_impl&<_Dtor&>(type, dtor);
} catch(...) {
dtor(type);
}
}
~auto_free() {
if(_pi) delete _pi;
}
operator _Type() {
if(_pi == 0)
throw;
return _pi-&>get();
}
};

因為虛函數+模板是我對於模板掌握最熟的東西,所以這是我最先想到的方式
其實這種方式我最先實現是這樣的:

class auto_free
{
struct auto_free_base {
virtual ~auto_free_base(){};
};
template &
class auto_free_impl : public auto_free_base {
_Type _type;
_Dtor _dtor;
public:
auto_free_impl(_Type type, _Dtor dtor) : _type(type), _dtor(dtor){}
~auto_free_impl() { _dtor(_type); }
};

auto_free_base *_pi;
public:
template&
auto_free(_Type type, _Dtor dtor) {
try {
_pi = new auto_free_impl&<_Type, _Dtor&>(type, dtor);
} catch(...) {
dtor(type);
}
}
~auto_free() {
if(_pi) delete _pi;
}
};

這樣實現,使用起來其實更簡單一些,但他有一個問題,就是對引用支持不好,例如:
Object obj;
auto_free _af(obj, FreeObj);

這樣在Object上將發生2次拷貝構造,而現在保留的方式雖然要多給出一個模板參數,但對於引用可以這樣使用:
auto_free& _af(obj, FreeObj);

當然,可以使用一些第三方的庫來使用廢棄的方式,達到完美支持引用的目的,如Boost的ref,如:
auto_free _af(boost::ref(obj), FreeObj);

另外,已廢棄的方式不能獲得其內部信息,也是廢棄他的原因之一,如以下方式:
auto_free& _af(malloc(5), free);
void *p = ((void *)_af);

方式二:將難題拋給用戶:

template &
class auto_free{
_Type _type;
_Dtor _dtor;
public:
auto_free(_Type type, _Dtor dtor) : _type(type), _dtor(dtor){}
~auto_free() { _dtor(_type); }
operator _Type() const {
return _type;
}
_Type get() {
return _type;
}
};

這種方式,簡單,明白,就是對用戶不夠友好, 需要用戶有一些使用基礎,如:
auto_free& _af(CreateBitmap( 10, 10, 1, 16, 0 ), DeleteObject);

Object obj;
Object_Free objFree;
auto_free& _af(obj, objFree);

方式三:採用boost的function對象:
template&
class auto_free
{
typedef boost::function& _Dtor;
_Type _type;
_Dtor _dtor;
public:
auto_free(_Type type, _Dtor dtor) : _type(type), _dtor(dtor){}
~auto_free() { _dtor(_type); }
_Type get() {
return _type;
}
};

怎麼樣,簡潔,強大,function支持函數,仿函數,成員函數(通過boost::bind);
而其性能採用簡單對象和複雜分離的方式,對於函數指針,仿函數,其效率幾乎和第二種方式一樣;

例子:
Object obj;
Object_Free objFree;
auto_free& _af(obj, objFree);

auto_free&_af(malloc(5), free);

後記:
方式一因為使用了虛函數,效率很差,耗時幾乎達到手動方式的6~7倍;
方式二是這3種中,效率最高的,他幾乎和手動釋放的方式一樣;
方式三僅僅比方式二差一點點;但他更易用一些;


不是近乎欽定,就是欽定。b.s.的每本書都在強調raii。在沒有uniqueptr和move的時代,raii還有許多不如人意的地方。c++委員會以前也知道,並且做了些不成功的嘗試,例如autoptr。前不久剛剛結束的cppconn2015,委員會強化了raii並且認為這是c++在資源管理方面區別於其他所有語言最大不同。為了和傳統代碼兼容,還增加了owner_ptr。b.s.和hsutter都做了raii方面的報告。所以raii不只是欽定,raii是c++的靈魂。


因為爽!

有了這東西,很少出現內存、資源泄露這類問題,在中途return、break的時候也不用幾行釋放資源的代碼到處copy。

namespace cxxdetail
{
template &
class InnerScopeExit
{
public:
InnerScopeExit(const FuncType _func) :func(_func){}
~InnerScopeExit(){ if (!dismissed){ func(); } }
private:
FuncType func;
bool dismissed = false;
};
template &
InnerScopeExit& MakeScopeExit(F f) {
return InnerScopeExit&(f);
};
}

#define DO_STRING_JOIN(arg1, arg2) arg1 ## arg2
#define STRING_JOIN(arg1, arg2) DO_STRING_JOIN(arg1, arg2)
#define SCOPEEXIT(code) auto STRING_JOIN(scope_exit_object_, __LINE__) = cxxdetail::MakeScopeExit([](){code;});

void copy(const char* src_file_path,const char* dst_file_path)
{
FILE* fp_src = fopen(src_file_path, "rb");
SCOPEEXIT(if (fp_src){ fclose(fp_src); });
if (!fp_src)
{
return;
}

//get file length
fseek(fp_src, 0, SEEK_SET);
fseek(fp_src, 0, SEEK_END);
const long longBytes = ftell(fp_src);
if (longBytes &<= 0) { return; } fseek(fp_src, 0, SEEK_SET); //do copy FILE* fp_dst = fopen(dst_file_path, "wb"); SCOPEEXIT(if (fp_dst){ fclose(fp_dst); }); if (!fp_dst) { return; } const int buffer_len = 4*1024; char buffer[buffer_len]; int read_len = 0; while ((read_len = fread(buffer, 1, buffer_len, fp_src)) &> 0)
{
fwrite(buffer, 1, read_len, fp_dst);
}
}


C++使用RAII也需要符合基本法。


在我這個普通C++程序員來看,RAII很好用,它在很大程度上避免了資源泄漏,減少了代碼的複雜度。所以RAII是個很好的特性。值得在C++代碼中大力推廣。


其實,沒有RAII的語言寫出來的模塊就像 拉完屎自己不擦屁股,要人家幫忙擦。


RAII的靈活性是 C++對象複雜的生命周期實現帶來的, 當然對象生命周期的表現還是很簡潔靈活的。 想想那些實現確定性析構的STEP的記錄點都讓人崩潰

為什麼其他語言沒有? 因為他們沒有這種對象生命周期管理啊

比如Java 對象永久存在,內存GC管理,資源本來應該可以實現自動管理的,可是`dispose`不說了。

C#比Java好一點,通過實現一些dispose介面能夠使用USING這樣的方式接近RAII管理資源


因為和有GC的語言不同,C++有確定性的析構的語義,所以RAII是最好的選擇。

Java,C#這種帶GC的語言都有finally關鍵字,因為這類語言都沒有確定的析構的語義,所以需要finally作為補充,畢竟靠GC沒法搞定網路/資料庫連接,文件等資源的管理。


如果你寫過資源管理相關的代碼,你就能深刻體會到raii 的好處了

資源包括不限於 內存,cpu ,埠,進程等等等。 帶gc的語言能讓你不用關心內存的管理,那其他資源呢? 使用完不釋放,不是一樣資源泄露么?

此時可以仔細想想,是否有比raii 更好的解決方案。


寫一堆腦殘對話就得這麼多贊了?因為RAII是管理資源不單是內存。其他管理內存的語言管理資源如文件打開時好看點的也要用using語句。RAII則比較統一


推薦閱讀:

明明很多編程語言從對象數組中取數據比用SQL從表中取數據方便,為什麼資料庫還是使用SQL?
為什麼Python程序不怎麼佔用CPU資源?
假如DNA是一種編程語言,那麼誰能保證人類對DNA的修改沒有bug?
CPU空操作的原理是什麼?
如何評價 VBA 語言?

TAG:程序員 | 編程語言 | C | GC垃圾回收計算機科學 |