標籤:

為什麼C++中的RAII類一般都是不可複製的?


其實不是 RAII 類一般不可複製,而是大多數類一般都不可複製。鑒於 C++ 各種隱式的拷貝、構造過程容易產生混淆,常見的簡單可靠的做法是盡量禁用隱式的複製行為,而讓有複製需求的類實現 clone() 方法。


這樣可以用最簡單幹凈的語義(資源有效期=對象生命周期)覆蓋大部分使用場景。剩下的場景可以用別的方法解決。正所謂殺雞焉用牛刀,雖然兩把刀看起來比一把刀複雜,但程序行為更容易把握。

真要複製當然有辦法:

1. shared_ptr。基於上述理由,shared_ptr&比RAII_object內嵌shared_ptr好。

2. 複製資源本身。比如handle可以dup(2)。但是這種動作比一般C++對象的複製要重,清晰起見最好用單獨的方法而不是operator=來做。


那句話怎麼說的來著?所有跳過「是不是」直接問「為什麼」的都是耍流氓。一個類有沒有用 RAII 和它可不可複製並沒有直接關係。

不信,你看標準庫里常用的 std::vector、std::map、std::string、std::shared_ptr 等等不都是用了 RAII 且可以複製的嗎?

RAII 只是一種資源管理的技巧而已,為的是把資源的生命周期和變數的生命期綁定起來,並沒有是否可複製的約束:

  • 實現時,把資源封裝進類:
    • 構造函數:獲取資源,並完成其它初始化。無法完成時拋出異常。
    • 析構函數:釋放資源。
  • 使用時,變數應該具有自動存儲期。

對象是否可複製,與它是否使用 RAII 技巧無關,卻與它本身是「值語義」還是「引用語義」相關。

  • 值語義:關心這個對象所表示的值。在複製時,類似於使用普通的 int,使用的是其值,複製前後的對象沒有關係。例如 std::string 對象就是值語義的,雖然使用了 RAII,但是允許複製。
  • 引用語義:關心對象本身。複製一個具有引用語義的對象通常是沒有意義的。例如 std::thread 對象,代表一個線程,複製這個對象的話,理論上需要創建一個完全相同的線程才行。所以 std::thread 雖然使用了 RAII,但是不允許複製。

當然,上面說的其實都是「理論上該不該可複製」。實際實現時,決定某個類是否可複製的,除了以上這些理論上的東西,還會考慮實現起來必不必要、麻煩不麻煩、中午吃得飽不飽、目前心情好不好、晚上是滿月還是弦月等等因素……

最後,如果不清楚某個類是否應該可複製,默認把它寫成不可複製,必要時才讓它可複製,通常是一個不錯的做法。


RAII類通常是用來管理資源的,而一般來說,資源管理從概念上講是獨佔性的。

這是由於資源本身的特殊性造成的。資源一般來說是一個句柄(或者說指針),它的生存周期是由它的持有者(管理者)所決定的。那麼當存在多個管理者管理同一個資源時,相互之間就必須要進行一些協調工作。

RAII類屬於簡單的資源管理者,類和類之間一般來說是不會有複雜的協調工作的。因此當一個RAII類獲得了一個資源之後,實際上獨佔了該資源的管理權(所有權)。這樣在自身析構的時候,只需要進行簡單的釋放工作。

如果RAII類可以複製,那麼我們就必須要求它具有共享的資源管理能力 —— 例如std::shared_ptr,使用引用計數作為資源管理者之間的協調機制。否則在RAII類想釋放它管理的資源時,如何保證沒有其它類持有這個資源呢?

因此,對於非共享的RAII類,即構造時獲得資源,析構時簡單釋放的類,通常只應該具有移動語義,而不應該具備拷貝語義。


以內存為例,RAII類會在構造函數中申請內存,在析構時自動釋放,如果可以複製就會釋放兩次


推薦閱讀:

如何評價博客園上的博文《 開發人員要亡新浪微博,你攔都攔不住!》?
C/C++ 中 0 與 NULL 區別是什麼?用 delete 時,用 p=0,還是用 p=NULL 好?為什麼?
考慮R值引用,c++下多字元串連接,如何寫更高效?
C#轉C++開發,該歷經怎樣的學習路線?
如何評價Qt Lite Project?

TAG:C |