標籤:

關於C++右值及std::move()的疑問?

在網上看了一篇介紹C++右值引用和move的文章,裡面的一段代碼讓我不解:

std::cout&<&<"test std::move: "; std::string str5 = "asdf"; std::string lr5 = str5; std::string rr5 = std::move(str5); rr5[0] = "b"; lr5[1] = "z"; std::cout&<&

首先這代碼能編譯,用g++ 4.9,C++11 std,結果是三個"bzdf"

我的疑問是:

1.我的理解是,str5被move之後,就不能再對str5和lr5進行讀訪問了,其行為應該是undefined的吧?按primer里的原話(It is essential to realize that the call to move promises that we do not intend to use moved-from object again except to assign to it or to destroy it. After a call to

move, we cannot make any assumptions about the value of the moved-from object.)所以上面代碼中move之後再對lr5和str5的訪問,都是undefined的吧?

2.如果上面代碼是合乎C++標準的,那為什麼會輸出三個"bzdf"?

謝謝


std::move 並不是 move,它只是一個cast而已,它可以幫助我們觸發move semantics. 我感覺這或許是C++11中很重要,但是卻又讓人誤解的一個詞。對於std::move,其實現如下:

template&
decltype(auto) move(T param)
{
using ReturnType = remove_reference_t&;
return static_cast&(param);
}

所以,它並未做任何實質的動作,所以你上面的代碼,通過右值引用拿到了str5,但是str5的值根本就沒有丟掉。

那為什麼C++ Primer會有你談到的一句話呢?那是因為std::move常常被用於觸發move constructor和move assignment operator,即move semantics. 而在這裡的行為則會上演類似這樣的場景

string(std::string src)
{
data = src.data;
src.data = nullptr;
}

所以,若你在你的代碼使用:

std::string rr5 = std::string(std::move(str5));

你會發現,如你所願,str5的身體被掏空了。

順便贈送std::move與rvo的對比,我去年的文章了:RVO V.S. std::move (C/C++ Cafe) 不過我會建議你在使用g++和clang++的時候,加上-fno-elide-constructors來進行實驗,很多問move沒有被調用的問題都大部分是這個原因,而我則因為某些不可描述的原因(ibm xlC編譯器沒有這個選項)而沒有辦法在文章中說明這個選項。


因為std::move只是將參數轉換為右值引用而已,它從來不會移動什麼.比如說一個簡單的std::move可以實現成這樣:

template &
decltype(auto) move (T param)
{
using ReturnType = remove_reference_t&;
return static_cast& (param);
}

真正的移動操作是在移動構造函數或者移動賦值操作符中發生的:

T (T rhs);
T operator= (T rhs);

std::move的作用只是為了讓調用構造函數的時候告訴編譯器去選擇移動構造函數.

所以只要把

std::string rr5 = std::move(str5);

改成

std::string rr5 = std::move(str5);

就會真的發生移動操作了.

Overview about rvalue references, move semantics and perfect forward


move之後,只是表示該對象已可以被「掏空」,這個過程純屬打了一個語法標記(取了左值的一個右值引用)。

你如果不對它動手動腳,那麼該對象就像什麼都沒發生一樣,正常的析構,死亡。

問題主需要了解,為什麼會出現std::move, 他是為了什麼目的而出現的,才能理解他的用法。


藍色的答案里引用的那篇文章的最後一段代碼是錯誤的,實際工程環境一定一定不要那麼寫。

BigObject foo(int n) {
BigObject localObj;
return std::move(localObj); // bad: DON"T do this
}

其他的答案講了為什麼move不會發生,但是並沒有說為什麼這麼設計。

之所以move語義只能作用於右值,是因為右值用完了就扔掉了。所以move語義把右值裡面的資源搬過來。所以在對真正的右值對象進行操作的時候,並不需要std::move。 編譯器知道這個對象在這次操作結束就可以被析構了。

例如return返回一個局部對象:

Foo bar() {
Foo baz;
return baz;
}

或者匿名的臨時變數:

Foo bar, baz;
Foo qux = bar + baz;

bar + baz這個表達式就是右值,複製給qux之後就沒有辦法再被訪問到了,所以這個右值內的資源可以被qux用,只要保證move結束後這個對象是一個合法的狀態,可以安全地被析構即可。

但是有些情況下我們知道一個對象不會被使用了,但是編譯器不知道。那我們就需要用std::move告訴編譯器這個對象內的資源是可以被搬走的。例如:

std::string copy = std::move(str5);

這個時候std::move(str5)返回一個str5的右值引用std::string,會強制觸發move語義。在這個語句之後就不要再訪問str5了(你可以,但是我認為這是不好的practice)。但是如果是

std::string rr5 = std::move(str5);

rr5僅僅是str5的一個右值引用,move並沒有發生,所以str5仍然是原來的值。

Bonus question:

是否存在編譯器認為對象不會被訪問,但是實際上被訪問的情況呢?有的,但是請一定要避免這麼做。比如,能不能在//...處加入代碼使得baz在return之後,析構之前被訪問?

Foo bar() {
Foo baz;
// ...
return baz;
}


std::move的函數參數是一個引用(可以是左值引用,也可以是右值引用),所以調用std::move時實參不會被拷貝,返回的是參數的右值引用類型(通過static_cast來強制轉換)。

所以:

std::string rr5 = std::move(str5);

之後, rr5, lr5所引用的實例都是 str5。

可以看一下std::move的實現,也就明白是怎麼回事了。


一般情況下,我們使用 str5,當參數:

printf( str5 )

當賦值

auto x=str5

它都不是作為右值引用在用的,std::move(str5) 返回的才是 str5 的右值引用,它僅僅是返回了一個右值引用,並沒有對對象本身做任何事。

對象作為右值引用賦值給另一個對象的時候,賦值時才會觸發移動構造函數,或者移動賦值函數。

移動構造函數僅僅是通過代碼,把一個對象的內容,移動到另一個對象里,原來的對象仍然是完整的,只是好比調用了 clear 被清空了。

而具體到你貼的代碼,從頭到尾都沒有任何移動構造行為,因此 str5 的內容甚至並沒有被清空。

lr5, rr5 都僅僅是引用,而不是對象。


通過彙編觀察下,反正引用到彙編階段行為和指針一模一樣,目前c/c++編譯器引用就是通過指針實現的。


推薦閱讀:

C++11 中 typedef 和 using 有什麼區別?
C++中如下的變數聲明見過嗎?
auto recommended = 200"000U;

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

TAG:C |