標籤:

C++ 函數返回局部變數的std::move()問題?

代碼如下

#include&
using namespace std;

class A{};

A fun()
{
A a;
return std::move(a);
}
int main()
{
auto f = fun();
return 0;
}

使用clang++-3.8 -Wall 編譯的時候,會報警告

warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]

使用g++-6 -Wall編譯時沒有警告。

請問這種寫法是否不符合規範,哪種寫法是正確的,且符合此處「移動」的目的?

謝謝。


想借題主這個問題把copy elison和move這件事好好說一下,因為很多剛了解c++的朋友經常入這個坑。

編程時經常會寫的一種函數叫做named constructor,這種函數的返回值是某個類的實例,其實本質上就是一種構造函數,但是因為可能需要在構建時執行一些其他的步驟,所以沒有寫成constructor的形式。比如:

User create_user(const std::string username, const std::string password) {
User user(username, password);
validate_and_save_to_db(user);
return user;
}
void signup(const std::string username, const std::string password) {
auto new_user = create_user(username, password);
login(user);
}

這裡create_user就是一個named constructor。

一些c++初學者可能會覺得這個代碼不夠優化,因為按照這個代碼字面意思來理解,create_user創建先創建了一個user,然後返回時又把user賦值給new_user,這個賦值會copy user裡面的內容,如果user很大的話(很有可能user裡面存了很多信息,比如username這種string的類型),這樣太慢(copy string可能還需要多做一次malloc)。

這樣難免一些人會想用c++11引入的move來優化,因為create_user裡面的user return了之後就沒用了,我可以把user move到new_user這樣不就省掉了copy時很大的開銷了么(比如user裡面的username就不需要malloc新內存也不需要一個個字元copy了)。

但事實上,在這種簡單的情況下編譯器比你更聰明,編譯器可以直接把user創建在new_user里,所以user只被創建一次,沒有任何copy開銷,user和new_user經過編譯器優化之後其實是同一個variable!這種優化就叫做copy elision。但是很不幸的是,如果用戶想自己用move優化的話,編譯器就不用做copy elision了,只能乖乖地按照用戶說的來,先創建一個user,然後在調用User的move constructor來創建new_user。這樣肯定比前一種開銷大很多。這就是為什麼clang非常「聰明」地給題主的例子給了一個warning。

接下來我們再說說我們怎麼能知道編譯器會不會對我寫的函數做copy elision的優化呢?有沒有可能我寫的函數邏輯特別複雜,編譯器沒法優化呢?如果有的話,我如果寫return move(a)不就會比copy更快了嗎?

這個邏輯是正確的,編譯器其實很傻,一旦create_user裡面的邏輯太複雜,編譯器可能就沒辦法分析出你能不能用一個變數取代兩個(user和new_user),那它就不做copy elision了。這時候用move就合情合理。

那到底什麼時候應該move,什麼時候應該依靠copy elision呢?通常主流的編譯器都會100% copy elision以下兩種情況:

1. URVO(Unnamed Return Value Optimization):函數的所有執行路徑都返回同一個類型的匿名變數,比如

User create_user(const std::string username, const std::string password) {
if (find(username)) return get_user(username);
else if (validate(username) == false) return create_invalid_user();
else User{username, password};
}

這裡所有的return都返回一個User類型,且每個返回的都是一個匿名變數。那編譯器100%會執行copy elision。

2. NRVO(Named Return Value Optimization):函數的所有路徑都返回同一個非匿名變數,比如

User create_user(const std::string username, const std::string password) {
User user{username, password};
if (find(username)) {
user = get_user(username);
return user;
} else if (user.is_valid() == false) {
user = create_invalid_user();
return user;
} else {
return user;
}
}

這裡因為所有路徑都返回同一個變數user。編譯器100%會執行copy elision。

其他的情況編譯器可能都不會使用copy elision的優化。


return statement

Copy elision - cppreference.com

關於 @神奇先生 的答案個人認為有些地方可以補充。

首先 URVO 在 C++17 是強制的。不過 NRVO 不是強制,意味著有時不這麼優化也是允許的。

其次,若 return 的表達式是符合返回類型的左值,且編譯器沒有進行複製省略,那麼標準(C++11 開始)也要求編譯器先試圖把表達式當右值,優先匹配移動構造函數(再匹配通常的複製構造函數),若失敗的話則再將其當左值,匹配接受非 const 引用的複製構造函數。

所以按照標準,上面的 std::move(a) 是不必要的(除非你希望強制調用移動構造函數),編譯器在必要時會做同樣的處理。


clang 只是想告訴你這是 premature pessimization。錯倒是沒錯的。

如果你想強制要求編譯器調用移動構造函數,而不是把這個過程優化掉,這麼寫是毫無問題的。


符合規範,這裡是說可能會影響copy elision優化,不過不是什麼大問題

關於copy elision: http://en.cppreference.com/w/cpp/language/copy_elision


C++11返回值會自動優化,有可能是move,也有可能是更加優化的做法,比如rvo,不用你手動寫,手動寫就強制要求move了

順便提一句,大多數情況下,返回T是錯誤的,不要以為返回了右值就要寫,不是


Scott Meyers 說:

Never apply std::move or std::forward to local objects if they would otherwise be eligible for the return value optimization.

翻譯過來就是:

請你不要老是想著幫編譯器優化 因為你沒有編譯器聰明 不然你就試試看!!


推薦閱讀:

在開發大C++工程的時候如何判斷和避免循環include?
為什麼C++使用sizeof關鍵字的時候不需要include <cstddef>頭文件就可以使用?
如何在C++中拋出一個編譯錯誤?
C++的RAND函數生成的值為什麼存在嚴重的不隨機性?
在c語言中,使用函數指針是否可以提高函數的調用速度 ?

TAG:C | CC | GCC | Clang |