2013-05-08 85 views
2

複製構造函數傳統上在C++程序中無處不在。不過,我懷疑自從C++ 11以來是否有充分的理由。我們可以說再見覆制構造函數嗎?

即使在程序邏輯並不需要複製對象,拷貝構造函數(通常爲默認值。)往往包括了對象再分配的唯一目的。如果沒有複製構造函數,則無法將對象存儲在std::vector中,甚至不能從函數返回對象。

但是,自從C++ 11以來,移動構造函數一直負責對象的重新分配。

複製構造函數的另一個用例是簡單地創建對象的克隆。不過,我很相信,一個.copy().clone()方法更適合這個角色不是一個拷貝構造函數,因爲...

  1. 複製對象是不是真的司空見慣。當然有時候對象的接口有時需要包含一個「自己創建一個副本」的方法,但有時候只是這樣。當情況是這樣的時候,顯式比隱式更好。

  2. 有時對象可能會暴露幾種不同的.copy()類似的方法,因爲在不同的上下文中,可能需要以不同的方式創建副本(例如,更淺或更深)。

  3. 在某些情況下,我們希望.copy()方法可以做與程序邏輯有關的非平凡事情(增加一些計數器,或者爲副本生成一個新的唯一名稱)。我不會接受任何複製構造函數中具有非顯而易見的邏輯的代碼。

  4. 最後但並非最不重要的一個.copy()方法可以是虛擬的,如果需要,允許解決slicing的問題。


那裏其實我想用一個拷貝構造函數的唯一情況是:

  • 可複製資源的RAII手柄(很明顯)
  • 結構,其意在像內置類型一樣使用,如數學向量或矩陣 -
    僅僅是因爲它們經常被複制,並且vec3 b = a.copy()太冗長。

附註:我認爲複製是需要CAS構造的事實,但需要operator=(const T&)我認爲確切的同樣的理由冗餘築底CAS;如果你真的需要這個
.copy() + operator=(T&&) = default將是首選。)

對於我來說,這是相當足夠的激勵默認使用T(const T&) = delete無處不在,在需要的時候提供一個.copy()方法。 (也許還有一個private T(const T&) = default只是爲了能夠寫copy()virtual copy()沒有樣板。)

問:上述推理是否正確,或者我錯過了爲什麼邏輯對象實際需要或以某種方式從複製構造函數中受益的任何好理由?

具體來說,我正確的移動構造函數完全接管了C++ 11中對象重新分配的責任嗎?當一個對象需要在內存中的其他位置移動而不改變其狀態時,我正在非正式地使用「重新分配」。

+3

「複製對象並不是很平常。」 - 我認爲考慮到C++默認使用值語義是很常見的。具有相同值的兩個對象應表示相同的事物。 – 2013-05-08 19:04:46

+3

默認禁用複製可能有一定的優點;但爲什麼你會通過成員函數啓用它,而不是複製構造函數的慣用使用呢? – 2013-05-08 19:09:18

+0

@sftrabbit:不一定:值不是多態的,引用和指針是。如果你想在C++中使用運行時多態性,你可以通過地址來標識對象,而不是值(它只表示「狀態」,而不是「標識」)。兩個具有相同'm_name'的'Person's不是相同的'Person'。 – 2013-05-09 06:28:34

回答

2

短前面回答

是上述推理正確的還是我失去了爲什麼邏輯對象的實際需要或以某種方式從拷貝構造函數中受益任何好的理由?

自動生成的拷貝構造函數在分離資源管理和程序邏輯方面有很大的好處;實現邏輯的類不需要擔心分配,釋放或複製資源。

在我看來,任何替代品都需要做同樣的事情,並且爲指定功能做這件事感覺有點奇怪。

龍答案

在考慮複製語義,它以類型劃分爲四類有用:

  • 原始類型,由語言定義的語義;
  • 資源管理(或RAII)類型,有特殊要求;
  • 聚合類型,它簡單地複製每個成員;
  • 多態類型。

原始類型是他們是什麼,所以他們超出了問題的範圍;我假設對語言進行徹底改變,打破幾十年的遺留代碼,將不會發生。多態類型不能被複制(同時保持動態類型),沒有用戶定義的虛函數或RTTI shenanig,所以它們也超出了問題的範圍。

所以建議是:要求RAII和聚合類型實現一個命名函數,而不是複製構造函數,如果它們應該被複制。

這對RAII類型沒有什麼區別;他們只需要聲明一個不同名稱的複製函數,而用戶只需稍微冗長些。

但是,在當前的世界中,聚合類型根本不需要聲明顯式的拷貝構造函數;會自動生成一個以複製所有成員,或者如果有任何成員是不可複製的,將被刪除。這確保了只要所有成員類型都可以正確複製,聚合也是如此。

在你的世界裏,有兩種可能性:

  • 無論是語言知道你的複製功能,並能自動生成一個(也許只有在明確要求,即T copy() = default;,因爲你要明確性)。在我看來,在其他類型中自動生成基於相同命名函數的命名函數,與目前生成「語言元素」(構造函數和操作符重載)的方案相比,更像是魔法,但也許這只是我的偏見而已。
  • 或者它留給用戶來正確實現聚合的複製語義。這很容易出錯(因爲你可以添加一個成員並忘記更新函數),並且打破了資源管理和程序邏輯之間當前清晰的分離。

,並解決您做出點主張:

  1. 複製(非多態性)對象司空見慣,但像你說的,現在是不常見的,它們可以被移動時可能。這只是你的意見,「明確更好」或T a(b);不明確比T a(b.copy());
  2. 同意,如果一個對象沒有明確定義的複製語義,那麼它應該有命名的功能,以涵蓋它提供的任何選項。我不明白這是如何影響如何複製普通對象的。
  3. 我不知道爲什麼你認爲複製構造函數不應該被允許做一個命名函數可以做的事情,只要它們是定義的複製語義的一部分。你認爲不應該使用複製構造函數,因爲你自己對它們進行了人爲限制。
  4. 複製多態對象是完全不同的一堆魚。強制所有類型使用命名函數是因爲多態性函數必須不會賦予您似乎爭論的一致性,因爲返回類型必須不同。多態副本將需要動態分配並通過指針返回;非多態副本應該按值返回。在我看來,使這些不同的操作看起來相似而不可互換是沒有價值的。
+0

謝謝,邁克!我重視自動複製ctors作爲編寫重要或不重要的複製例程的幫助。我的建議是1)將自定義副本留給RAII類型,2)將公共缺省副本留給聚合,3)在其他地方提供副本()或虛擬副本()而不是公共副本。有一件事我忽略了多態拷貝需要依賴動態分配的內存。現在我可以看到,提供命名複製方法仍然無法統一多態與非多態非聚集之間的複製接口(或者如前所述的「邏輯對象」)。 – Kos 2013-05-11 14:51:43

+0

如果程序邏輯要求以非平凡的方式複製對象,我仍然主張的建議是使用命名方法。在這種情況下,我仍然堅持要有一個有名的方法,並且沒有公開的副本。爲什麼?當唯一的意圖是重新分配而不是邏輯相關的重複時,犯這個錯誤並使用copy ctor而不是移動ctor會比較容易。它可以簡單地省略一個「noexcept」。複製構造函數由於它們的雙重角色(邏輯相關和重新分配相關)而仍然很棘手 - 這個事實讓我陷入了整個思路。 – Kos 2013-05-11 14:52:08

+0

這實際上有一個有趣的延續 - 是*任何*邏輯相關的非聚集實際「微不足道」的副本?也許不是;我需要消化。如果我拿出一個令人信服的例子,我想回到你身邊。 – Kos 2013-05-11 15:01:51

4

問題是什麼是「對象」這個詞是指什麼。

如果對象是變量是指(如在java或C++中通過指針,採用經典的OOP範式)每個「變量之間複製」是一個「共享」的資源,並且如果是單所有權強加的,「分享「變成」移動「。

如果對象是變量本身,因爲每個變量都必須有自己的歷史記錄,所以如果你不能/不希望強迫另一個變爲破壞值,就不能「移動」。

Cosider例如std::strings

std::string a="Aa"; 
    std::string b=a; 
    ... 
    b = "Bb"; 

你期望的a值改變,或該代碼不編譯?如果不是,則需要複製。

現在考慮這個:

std::string a="Aa"; 
    std::string b=std::move(a); 
    ... 
    b = "Bb"; 

現在是空的,因爲它的價值(更好的,包含它的動態內存)已經「轉移」到b。然後b的值被加密,並丟棄舊的"Aa"

從本質上說,此舉只是工作,如果顯式調用,或者如果正確的說法是「暫時的」,就像在

a = b+c; 

其中顯然不是分配後需要通過operator+返回的資源保持,因此,將其移動到a,而不是將其複製到另一個a的持有位置,並刪除它更有效。

移動和複製是兩回事。移動不是「複製的替代品」。它是一種更有效的方法,只有在對象不是需要才能生成其自身的克隆的所有情況下避免複製。

+0

謝謝!對象當然不同於變量,但C++爲變量的* value *提供了罕見的(和有用的)選項作爲對象 - 將對象的存儲與變量的統一。你的例子顯示了2個值類型的變量,這意味着2個實際的不同的對象應該在那裏,一個可能是另一個的副本。是的,我希望'string b = a'在編譯時失敗,'string b = a.copy()'成功。 – Kos 2013-05-09 06:39:42

+0

@Kos:你實際上可以做到這一點。但是C++並沒有強加給你。如果你想要一個所有對象都在堆上的框架,並且只有引用包裝器/智能指針在堆棧上,你當然可以做到這一點。但是這和java沒有太大的區別。那麼爲什麼不使用Java呢?不要回答。考慮一下,然後決定。上下文 - 在這種情況下 - 比其他事情更重要。但從語言的角度來看,那些「引用包裝器」或「智能指針」本身就是...值類。所以語言離不開它! – 2013-05-09 06:45:58

+0

@Kos:從一個慣用的角度來看,'strring b = copy(a)'應該可以更好地與'string b = move(a)'配對。複製和移動扮演「操縱者」的角色,而不是「成員」。 – 2013-05-09 06:50:14

1

複製構造函數有用的一種情況是在執行強壯異常保證時。

爲了說明這一點,我們考慮函數std::vector。該功能可以被粗略地實現如下:

void std::vector::resize(std::size_t n) 
{ 
    if (n > capacity()) 
    { 
     T *newData = new T [n]; 
     for (std::size_t i = 0; i < capacity(); i++) 
      newData[i] = std::move(m_data[i]); 
     delete[] m_data; 
     m_data = newData; 
    } 
    else 
    { /* ... */ } 
} 

如果resize功能均具有較強的異常保證,我們需要確保,如果有異常被拋出,std::vectorresize()調用之前的狀態保存。

如果T沒有移動構造函數,那麼我們將默認爲複製構造函數。在這種情況下,如果複製構造函數拋出異常,我們仍然可以提供強大的異常保證:我們只需deletenewData數組,並且已經完成對std::vector的處理。但是,如果我們使用T的移動構造函數,並且拋出異常,那麼我們有一堆T s被移入newData數組中。回滾這個操作並不是直截了當的:如果我們試圖將它們移回m_data數組,移動構造函數T可能會再次拋出異常!

要解決此問題,我們有std::move_if_noexcept函數。如果該函數被標記爲noexcept,則該函數將使用T的移動構造函數,否則將使用複製構造函數。這使我們能夠以提供強有力的例外保證的方式實施std::vector::resize

爲了完整起見,我應該提到C++ 11 std::vector::resize在所有情況下都不提供強大的異常保證。根據www.cplusplus.com我們有以下保證:

如果n小於或等於容器的大小,函數永遠不會拋出異常(無丟包保證)。 如果n較大並且重新分配發生,如果元素的類型是可複製或無法移動的,則在異常情況下(強保證),容器中不會有任何更改。 否則,如果引發異常,容器將保留有效狀態(基本保證)。

+0

謝謝,這是你在這裏提出的一個重要觀點。但是它是否真的正常(或者:是否永遠是正確的)拋出'swap',移動ctors並移動op = s?我甚至看到了一個[提案](http://www.open-std。org/jtc1/sc22/wg21/docs/papers/2009/n2855.html#nothrowmove)在這裏強制使用nothrow。 – Kos 2013-05-09 07:05:06

+0

@Kos:不幸的是,它處於C++的當前狀態。 – Puppy 2013-05-09 08:41:32

1

這是事情。移動是新的默認值 - 新的最低要求。但是複製仍然是一項非常有用和便利的操作。

沒有人應該向後彎曲以提供複製構造函數。但如果您可以簡單地提供它,它對於您的用戶具有可複製性仍然很有用。我承認,對於我自己的類型,只有當它變得清晰時,我纔會添加它們,而不是立即使用這些構造函數。到目前爲止,這是非常非常少的類型。