轉自coolmeme
通過了解std::move和std::forward不做什么來理解它們很有用。std::move不移動任何東西,std::forward也不轉移任何東西。在運行時(runtime),他們什么都不做,一行代碼也不產生。
std::move和std::forward僅僅是進行類型轉換的函數(實際上是函數模板)。std::move無條件的將其參數轉換為右值,而std::forward只在必要情況下進行這個轉換,就是這樣。這個解釋會引起一系列問題,但是基本上這就是完整的故事。
為了讓故事更加具體,這有一個在C++11中std::move的模擬實現:
template<typename T> // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = // alias declaration;
typename remove_reference<T>::type&&; // see Item 9
return static_cast<ReturnType>(param);
}
我高亮了兩處代碼。一個是函數的名字,因為返回值很繁瑣,我怕你失去忍耐。另一處是函數的精華本質部分:轉換。如你所見,std::move的參數為一個對象的引用(統一引用,詳見條款24),并且返回的也是該對象的引用。
函數返回值的“&&”部分表示std::move返回了一個右值引用,但是如條款28所述,假如類型T碰巧是個左值引用,T&&就會成為一個左值引用。為阻止這個發生,一個type trait(見條款9)std::remove_reference被用在T上,這樣可以確保“&&”可以應用在一個不是引用的類型上,這個很重要,因為函數返回的右值引用必須是右值。于是,std::move將其參數轉換右值,這就是它所有做的事情。
此外,std::move在c++14中的實現顯得更簡短。由于函數返回類型推導(條款3)和標準庫里的別名模板std::remove_reference_t (見條款9),std::move可以這樣實現:
template<typename T> // C++14; still indecltype(auto) move(T&& param) // namespace std{ using ReturnType = remove_reference_t<T>&&; return static_cast<ReturnType>(param);}
看上去更簡單些,不是?
因為std::move除了把參數轉換為右值以外不做別的事情,有建議給它一個更好的名字也許類似rvalue_cast,即使如此,我們現在擁有的名字是std::move,所以認識到std::move做什么和不做什么很重要。它做的是轉換,不做移動。
當然,右值適合被移動,所以std::move應用在一個對象上時就是告訴編譯器對象可以被移動。這就是為什么std::move擁有這樣的名字:使得指定可以被移動的對象更容易些。
事實上,右值是僅有的可以被移動的對象。假如你寫了個類代表注釋(annotation),類的構造函數使用std::string類型做為參數表示注釋內容,并且拷貝參數到一個成員變量里。根據條款41的信息,你聲明了一個傳值參數:
class Annotation {public: explicit Annotation(std::string text); // param to be copied,
… // so per Item 41,}; // pass by value
但是Annotation的構造函數僅僅需要讀取text的值,不需要改變它。根據我們固有的傳統,盡可能的使用const,你改變了聲明如下:
class Annotation {public: explicit Annotation(const std::string text) …};
為了避免當拷貝text到數據成員中時消耗一次拷貝操作,你使用了條款41的建議,將std::move應用到text上,于是產生了一個右值:
class Annotation {public: explicit Annotation(const std::string text) : value(std::move(text)) // "move" text into value; this code { … } // doesn't do what it seems to! …PRivate: std::string value;};
代碼編譯鏈接都可以正常,也把設置了數據成員為text的內容。唯一使得這段代碼和你想象中的完美實現不一樣的地方是text不是移動到value的,是拷貝的。當然,text是通過std::move轉換成一個右值,但是text是被聲明成const std::string,所以在轉換前,text是一個左值的const std::string,轉換的結果是一個右值const std::string,最終常量性保留了下來。
考慮下當編譯器必須決定哪個std::string構造函數必須調用時的效果,有兩個可能:
class string { // std::string is actually apublic: // typedef for std::basic_string<char> … string(const string& rhs); // copy ctor 拷貝構造函數 string(string&& rhs); // move ctor move移動構造函數 …};
在Annotation的構造函數的成員初始化列表里,std::move(text)的結果是一個類型為const std::string的右值。這個右值不能傳遞給std::string的move構造函數,因為move構造函數需要一個指向非常量std::string的右值引用作為參數。然而這個右值可以傳遞給拷貝構造函數,因為指向常量的左值引用是允許綁定到一個常量右值的。于是成員初始化就會調用std::string的拷貝構造函數,即使text已經轉換成一個右值。這樣的行為是維持常量正確性的一個必需,因為移動一個對象到另一個值通常會改變這個對象,所以語言就不應該允許常量對象被傳遞給一個能修改他們的函數(比如move構造)。
從這個例子可以學到兩個教訓。第一,假如你想對象能夠被移動,不要聲明對象為const,在const對象上的移動操作默默的被轉換成了拷貝操作。第二,std::move不僅不移動任何東西,甚至不能保證被轉換的對象可以被移動。唯一可以確認的是應用std::move的對象結果是個右值。
std::forward的故事和std::move的故事很類似,但std::move是無條件的轉換其參數為一個右值,而std::forward是在某些特定條件下進行轉換。std::forward是一個有條件轉換。為了理解什么時候轉換什么時候不轉換,回憶一下std::forward是怎么使用的。最常見的場景是在函數模板,參數是一個統一引用參數,這個參數會被傳遞給另一個函數。
void process(const Widget& lvalArg); // process lvalues
void process(Widget&& rvalArg); // process rvaluestemplate<typename T> // template that passesvoid logAndProcess(T&& param) // param to process{auto now = // get current timestd::chrono::system_clock::now();makeLogEntry("Calling 'process'", now);process(std::forward<T>(param));}
考慮兩種調用logAndProcess的情形,一種是左值,一種是右值:
Widget w;
logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue
在logAndProcess內部,參數param被傳遞給函數process,process被重載為左值和右值。當我們通過左值去調用logAndProcess時,我們很自然的期望那個左值可以同樣作為一個左值轉移到process函數,當我們通過右值去調用logAndProcess時,我們期望重載的右值函數可以被調用。
但是param,像所有函數參數一樣,是個左值。在logAndProcess內部每一個對process的調用會調起左值重載。為了避免這個,我們需要一個機制來把param轉換成一個右值,當且僅當傳入的用來初始化parm的實參(就是傳遞到logAndProcess的參數)是個右值。
你可能疑惑std::forward是怎么知道它的參數是通過一個右值來初始化的。比如在以上代碼中,std::forward是怎么知道param是被一個左值或右值來初始化呢?簡單的回答是這個信息被編碼到logAndProcess的模板參數T里面。這個參數被傳遞給std::forward,然后恢復了編碼的信息。詳細的描述見條款28。
既然std::move和std::forward都歸結為轉換,唯一不同就是std::move始終進行轉換,而std::forward僅僅有時候轉換,你可能會問我們是否可以廢棄std::move,而只用std::forward。從純技術角度來看,答案是肯定的:std::forward可以做到,std::move不是必須的。當然函數也不是必須的,我們可以寫轉換代碼,但是我希望我們會覺得那樣比較惡心。
std::move吸引人之處在于方便,減少了錯誤的可能性,而且更加清晰。考慮一個類,可以跟蹤我們使用了多少次的move構造函數。我們所需要的是一個靜態變量的計數器,在move構造函數使用時增加。假設類里面的唯一一個非靜態數據成員是std::string,這里有個方便的方法(也就是利用std::move)去實現move構造函數:
class Widget {public: Widget(Widget&& rhs) : s(std::move(rhs.s)) { ++moveCtorCalls; } …
private: static std::size_t moveCtorCalls; std::string s;};
用std::forward來實現相同的行為,代碼可能如下:
class Widget {public:Widget(Widget&& rhs) // unconventional,: s(std::forward<std::string>(rhs.s)) // undesirable{ ++moveCtorCalls; } // implementation…};
注意首先std::move只需要一個函數參數(rhs.s),而std::forward既需要一個函數參數(rhs.s),又需要一個模板類型參數(std::string)。然后我們注意到我們傳遞給std::forward的參數類型必須一個非引用的,因為編碼規范上說明了傳遞的參數必須是一個右值(見條款28)。std::move需要更少的打字,and it spares us the trouble of passing a type argument。也減少了我們傳遞一個錯誤類型(比如std::string&,這會導致數據成員s被拷貝構造而不是move構造)的可能性。
更重要的是,使用std::move會無條件轉換為一個右值,而使用std::forward只會把是綁定到右值引用的參數轉換成右值。這是兩個非常不同的行為。第一個是典型的move,而第二個是僅僅傳遞(轉移)一個對象到另一個函數,通過這種辦法來保留對象原始的左值性或右值性。因為兩者如此不同,所以最好我們使用不同的函數(名)來區分它們。
要記住的事情
1.std::move執行一個無條件的轉化到右值。它本身并不移動任何東西;
2.std::forward把其參數轉換為右值,僅僅在那個參數被綁定到一個右值時;
3.std::move和std::forward在運行時(runtime)都不做任何事。
新聞熱點
疑難解答