標籤:

operator=重載時,是否可以用一個按值傳遞版本取代按const引用傳遞和右值引用傳遞?

今天在看Scott Meyers的Effective C++

在ITEM12,Meyers提到operator=重載需要解決自我賦值問題和保證異常安全,並介紹了copy-and-swap idiom作為解決的辦法。

template&
stack& stack&::operator=(stack& that)
{
swap(that);
return *this;
}

上面是stack的operator=重載的例子,按值傳遞另一個stack,如果傳入左值就調用拷貝構造函數,右值則是調用移動構造函數,進入函數體後將*this與that交換,that作為臨時副本,在函數退出被析構。

我個人覺得這是比較完美的實現了,相比於另一個可行的版本

template&
stack& stack&::operator=(const stack& that)
{
stack& tmp(that);

swap(tmp);
return *this;
}

雖然Meyers說第一個版本犧牲了清晰性,但是我覺得它實現更漂亮一些,並且,Meyers說第一個版本會更加高效,高效在哪呢?

並且我想問它可以是否可以完全取代STL中的兩個版本:

stack operator=(const stack that);
stack operator=(stack that);

或者取代第一個,變為下面這樣行不行?

stack operator=(stack that);
stack operator=(stack that);

再延伸一點,同上,在實現了必要的複製構造函數和移動構造函數的情況下

void push(E elem);

可不可以取代

void push(const E elem);
void push(E elem);

如果可以的話,STL為什麼不增加這個呢?如果不可以那它有什麼缺陷嗎?希望解答,謝謝。


這個問題解釋了太多次我都不想再解釋了,事實上const T和T如果要分開,那證明你需要的邏輯是不一樣的。如果他們完全一致(包括對內存的操作),那你就應該只留一個T,不要有引用。

一般來講,你可能會覺得一個函數對const T和T分開重載會有好處,但是其實並不是這樣的,萬一你的函數有5個參數呢,你難道要提供32個重載嘛?畢竟我調用的時候可能在不同的地方有不同的參數是右值對吧?所以正確的做法是不要使用引用,全部用T。這樣有以下幾個好處:

  1. 不用重載
  2. 參數如果是右值就會被自動move
  3. 如果函數裡面需要把參數move走,可以直接move,因為那是值,不跟別人共享一個對象

當然你還要一些變通,譬如說你知道一個參數

  1. 絕對不可能是右值
  2. 他是右值沒有任何意義
  3. 在函數裡面你並不試圖把它複製走

那你就可以考慮T或者const T等舊社會的做法。

說到operator=,如果你要使用T的話,你就不需要其他重載了,因為:

  1. 如果傳進來的是左值,你反正都要複製一次的,你就乾脆複製在參數里,把參數move給自己,共複製1次
  2. 如果傳進來的是右值,那麼他會自動move進參數,然後把參數move給自己,共複製0次

你看,是不是跟T和T差不多?就是每個都多了一個move,但是你卻節省了一個重載。而通常move構造函數的代價應該視為0,因為只是複製幾個位元組而已。如果不是,那就是你做的不對。

如果你很在乎這一次move,反正operator=只有一個參數,重在兩次也不是很難接受,那你就重載唄。


可以,而且輪子哥的回答很多情況下是best practise。但是要考慮移不動的大棧對象,可能造成非常大的棧空間消耗

比如較大的array,比如高達5000位元組的mt19937


首先感謝各位的回答,了解了很多。

經 @Vigilans 的提醒,我去看了《Effective Modern C++》Item 41,發現我提的問題好像Meyers好像在Item 41已經說的差不多了。

在這裡列舉一下:

class Widget { // Approach 1:
public: // overload for
void addName(const std::string newName) // lvalues and
{ names.push_back(newName); } // rvalues
void addName(std::string newName)
{ names.push_back(std::move(newName)); }

private:
std::vector& names;
};
class Widget { // Approach 2:
public: // use universal
template& // reference
void addName(T newName)
{ names.push_back(std::forward&(newName)); }

};
class Widget { // Approach 3:
public: // pass by value
void addName(std::string newName)
{ names.push_back(std::move(newName)); }

};

三者的代價分析:

Approach 1: 左值一次複製,右值一次移動。

Approach 2: 左值一次複製,右值一次移動。

Approach 3: 左值一次複製和一次移動,右值兩次移動。

最終結論:對於可複製的,移動代價不高,並且總是要複製的參數,可以使用按值傳遞版本,便於實現,代碼也少。

所以權衡一下,我覺得還是像輪子哥說的那樣,pass by value一般應該是最合適的了。

不過我還是在考慮Approach 2的universal reference,也就是正式標準的forwarding reference,Meyers好像挺推薦這個的,而且STL中的emplace方法就是用的forwarding reference。

所以我想知道這種接受forwarding reference然後類型推導的方式有什麼不足嗎,總覺得對於模板類再額外加一個模板函數有點奇怪 @vczh


如果需要所有權就可以,如果你只是要用一下值就不需要。

比如std::size不可能設計為按值傳遞容器,而像題中那樣的用按值傳遞是可以的,另外如果你的對象使用移動語義沒有很大的性能提升的話也最好用引用,例如std::array


然而能想到重載operator=的類都是有點特殊應用的吧, 要不然為什麼不default. 默認編譯器是提供左右值兩個版本的. 題主討論的stack類, 目測是因為體量原因需要明確地表現出最少的拷貝開銷, 所以Meyers會提倡第二個版本. 這裡是你自己寫的情況, 越清晰越好, 而不是讓swap的兩個參數類型迷惑了. 而從stl的角度, 寫stl的妖精似乎不會有這種問題.

對於左右值, 首先沒什麼事情不要用右值, 除非你的類非常非常大或者要複製非常非常多非常非常頻繁, 要不然省那麼點開銷有過早優化嫌疑. 另外就算用的到, 也有vector之類的手段. 那剩下的問題就是題主說的左值和傳值的問題. 默認必須是左值啊, 除了那麼幾個基本類型, 必然是丟個地址過去節省. 這是99%的情況, 所以縱然猖狂如stl, 它也得盡量順從編譯器. 而自己寫的情況, 你是在函數實現上簡潔了, 但是你要通篇寫operator(T const) = delete, 而且同事看了會奇怪.

更正: operator(T const) = delete 可以不用寫的, 這個跟default一樣, 習慣問題, 想清晰就全寫出來沒意外, 想簡潔就不寫, 不過容易在維護的過程中忘記修改過operator=, 等你變主意不自己寫, 默認的又會回來, 而你可能需要修改默認的, delete可以保證編譯器報錯. 搞C++就是頭大.


然而很多時候我們並不希望多那次移動/複製啊


Effective Modern C++ Item 41就是專門討論按值傳參的問題的。

Mayer對其的主要態度大概可以用如下一段概括:

Usually, the most practical approach is to adopt a "guilty until proven innocent" policy, whereby you use overloading or universal references instead of pass by value unless it"s been demonstrated that pass by value yields acceptably efficient code for the parameter type you need.

他在文內也多次強調,並不能總是假設移動的開銷相對於複製是可以忽略不計的,同時,還應該考慮其他一些問題(slicing problem, 編譯器優化(如SSO短字元串優化)等)。

對於STL中為什麼左右值引用分開實現而不是按值傳參,個人的淺見是:

STL為了保證最大程度的擴展性和兼容性,是不能假設對傳進來的參數進行移動的開銷一定為0,甚至不能假設傳進來的參數是可以移動的(通常這時移動就變成了複製)

因此它在實現時,不應當無視按值傳參多出來的一次移動,而應分開實現來保證效率和適用性。


以為是==了 原來是=

===========================

我覺得其它回答根本沒有考慮operator==這個函數本身所隱含的語意

正所謂重載操作符也要講基本法

不同意 @vczh 的做法(其實 @vczh 大部分說的也不錯 但是不應該一律用T,T const 還是有意義的,除了move構造和move賦值以外參數類型應該要麼是T const 要麼就是T,這裡就應該適用T const 而不是 T)

對於operator==而言

它的含義是比較兩個對象本身是否相等

應該保證在函數整個執行期間都不對這兩個對象本身和外部環境產生任何副作用

所以不應該出現改變 哪怕是臨時改變任意一個對象的內容(也就是說在operator==函數執行過程中的任何時間點上 去訪問這兩個進行比較的對象 它們應該都不應該發生任何變化 應該完完全全線程讀取安全的)所以必須是const引用或者是副本

不應該出現任何潛在操作了外部環境的情況(這個不必多說 什麼在operator==里去操作std::cout都是不應該的)

也不應該把傳入的臨時對象引用給move了 這實際上也產生了副作用 因為原對象莫名其妙被你掏空(改變)了(哪怕原對象是臨時的 你把原對象掏空就是副作用 違反了不可改變原對象)

所以T 是不應該出現的(因為它既違反了不應該修改哪怕是臨修改對象內容 也違反了不能把它move了 哪怕傳入的是一個臨時對象)

而且operator比較的是兩個對象本身 而不是去和一個副本做比較(就算是move 本質也是產生一個副本 只是這個副本掏空了原本並取代了它而已)如果參數是T 就改變了這個函數的含義 這個函數的含義就成了拿a對象本身和b對象副本做比較 很顯然這和比較兩個對象本身是否相等是不同的 雖然結果相同 這不符合operator==的本意 而且還會引入額外的開銷

而且技術上來說如果T是一個既不可copy又不可move的類型 那參數類型是T就無法通過編譯了 所以不管是從函數語意上來說,還是技術上來說,還是開銷上來說參數都必須是原對象的引用而不是副本

綜上所述operator==的參數必須是T const


推薦閱讀:

關於C++右值及std::move()的疑問?
C++11 中 typedef 和 using 有什麼區別?
C++中如下的變數聲明見過嗎?
auto recommended = 200"000U;

C++的class與struct到底有什麼不同?
Google C++ Style Guide 中為什麼禁止使用預設函數參數?

TAG:C | CC | C11 |