關於C++的拷貝構造函數,很多的建議是直接禁用。爲什麼大家會這麼建議呢?沒有拷貝構 造函數會有什麼限制呢?如何禁用拷貝構造呢?這篇文章對這些問題做一個簡單的總結。
這裏討論的問題以拷貝構造函數爲例子,但是通常賦值操作符是通過拷貝構造函數來實現 的( copy-and-swap 技術,詳見《Exceptional C++》一書),所以這裏討論也適用於賦 值操作符,通常來說禁用拷貝構造函數的同時也會禁用賦值操作符。
爲什麼禁用拷貝構造函數
關於拷貝構造函數的禁用原因,我目前瞭解的主要是兩個原因。第一是淺拷貝問題,第二 個則是基類拷貝問題。
淺拷貝問題
編譯器默認生成的構造函數,是memberwise
拷貝^1,也就是逐個拷貝成員變量,對於 下面這個類的定義^2:
|
默認生成的拷貝構造函數,會直接拷貝buf_
的值,導致兩個Widget
對象指向同一個緩 衝區,這會導致析構的時候兩次刪除同一片區域的問題(這個問題又叫雙殺
問題)。
解決這個問題的方式有很多:
-
自己編寫拷貝構造函數,然後在拷貝構造函數中創建新的
buf_
,不過拷貝構造函數的 編寫需要考慮異常安全的問題,所以編寫起來有一定的難度。 -
使用
shared_ptr
這樣的智能指針,讓所有的Widget
對象共享一片buf_
,並 讓shared_ptr
的引用計數機制幫你智能的處理刪除問題。 -
禁用拷貝構造函數和賦值操作符。如果你根本沒有打算讓
Widget
支持拷貝,你完全可 以直接禁用這兩操作,這樣一來,前面提到的這些問題就都不是問題了。
基類拷貝構造問題
如果我們不去自己編寫拷貝構造函數,編譯器默認生成的版本會自動調用基類的拷貝構造 函數完成基類的拷貝:
|
上面這段代碼的輸出如下:
|
但是如果我們出於某種原因編寫了,自己編寫了拷貝構造函數(比如因爲上文中提到的淺 拷貝問題),編譯器不會幫我們安插基類的拷貝構造函數,它只會在必要的時候幫我們安 插基類的默認構造函數:
|
上面這段代碼的輸出如下:
|
|
這當然不是我們想要看到的結果,爲了能夠得到正確的結果,我們需要自己手動調用基類 的對應版本拷貝基類對象。
|
這本來不是什麼問題,只不過有些人編寫拷貝構造函數的時候會忘記這一點,所以導致基 類子對象沒有正常複製,造成很難察覺的BUG。所以爲了一勞永逸的解決這些蛋疼的問題, 乾脆就直接禁用拷貝構造和賦值操作符。
沒有拷貝構造的限制
在C++11之前對象必須有正常的拷貝語義才能放入容器中,禁用拷貝構造的對象無法直接放 入容器中,當然你可以使用指針來規避這一點,但是你又落入了自己管理指針的困境之中 (或許使用智能指針可以緩解這一問題)。
C++11中存在移動語義,你可以通過移動而不是拷貝把數據放入容器中。
拷貝構造函數的另一個應用在於設計模式中的原型模式
,在C++中沒有拷貝構造函數,這 個模式實現可能比較困難。
如何禁用拷貝構造
-
如果你的編譯器支持 C++11,直接使用
delete
-
否則你可以把拷貝構造函數和賦值操作符聲明成
private
同時不提供實現。 -
你可以通過一個基類來封裝第二步,因爲默認生成的拷貝構造函數會自動調用基類的拷 貝構造函數,如果基類的拷貝構造函數是
private
,那麼它無法訪問,也就無法正常 生成拷貝構造函數。class NonCopyable { protected: ~NonCopyable() {} // 關於爲什麼聲明成爲 protected,參考 // 《Exceptional C++ Style》 private: NonCopyable(const NonCopyable&); } class Widget : private NonCopyable { // 關於爲什麼使用 private 繼承 // 參考《Effective C++》第三版 } Widget widget(Widget()); // 錯誤
上不會生成memberwise
的拷貝構造函數,詳細內容可以參考《深度探索C++對象模型》一 書
禁用拷貝
禁用原因主要是兩個:
1. 淺拷貝問題,也就是上面提到的二次析構。
2. 自定義了基類和派生類的拷貝構造函數,但派生類對象拷貝時,調用了派生類的拷貝,沒有調用自定義的基類拷貝而是調用默認的基類拷貝。這樣可能造成不安全,比如出現二次析構問題時,因爲不會調用我們自定義的基類深拷貝,還是默認的淺拷貝。
Effective C++條款6規定,如果不想用編譯器自動生成的函數,就應該明確拒絕。方法一般有三種:
1. C++11對函數聲明加delete關鍵字:Base(const Base& obj) = delete;,不必有函數體,這時再調用拷貝構造會報錯嘗試引用已刪除的函數。
2. 最簡單的方法是將拷貝構造函數聲明爲private
3. 條款6給出了更好的處理方法:創建一個基類,聲明拷貝構造函數,但訪問權限是private,使用的類都繼承自這個基類。默認拷貝構造函數會自動調用基類的拷貝構造函數,而基類的拷貝構造函數是private,那麼它無法訪問,也就無法正常生成拷貝構造函數。
Qt就是這樣做的,QObject定義中有這樣一段,三條都利用了:
第一種方法:最簡單的方法是將拷貝構造函數聲明爲private
private:
Q_DISABLE_COPY(QMainWindow)
#define Q_DISABLE_COPY(Class) \
Class(const Class &) Q_DECL_EQ_DELETE;\
Class &operator=(const Class &) Q_DECL_EQ_DELETE;
類的不可拷貝特性是可以繼承的,例如凡是繼承自QObject的類都不能使用拷貝構造函數和賦值運算符。
(2)第二種方法 繼承一個uncopyable類
C++的編譯在鏈接之前,如果我們能在編譯期解決這個問題,會節省不少的時間,要想在編譯期解決問題,就需要人爲製造一些bug。我們聲明一個專門阻止拷貝的基類uncopyable。
class uncopyable{
protected:
uncopyable(){}
~uncopyable(){}
private:
uncopyable(const uncopyable&);
uncopyable& operator=(const uncopyable&);
}
接下來,我們的類只要繼承uncopyable,如果要發生拷貝,編譯器都會嘗試調用基類的拷貝構造函數或者賦值運算符,但是因爲這兩者是私有的,會出現編譯錯誤。