#Item 23:Understand std::move and std::forward
首先通過了解它們(指std::move和std::forward)不做什么來認識std::move和std::forward是非常有用的。std::move不move任何東西。std::forward也不轉發任何東西。在運行時,他們什么都不做。不產生可執行代碼,一個比特/Users/shikunfeng/Documents/neteaseWork/timeline_15_05_18/src/main/webapp/tmpl/web2/widget/event2.ftl的代碼也不產生。
std::move和std::forward只是執行轉換的函數(確切的說應該是函數模板)。std::move無條件的將它的參數轉換成一個右值,而std::forward當特定的條件滿足時,才會執行它的轉換。這就是它們本來的樣子.這樣的解釋產生了一些新問題,但是,基本上,就是這么一回事。
為了讓這個故事顯得更加具體,下面是C++ 11的std::move的一種實現樣例,雖然不能完全符合標準的細節,但也非常相近了。
```cpp
template<typename T>
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);
}
```
我為你高亮的兩處代碼(我做不到啊!--菜b的譯者注)。首先是函數的名字move,因為返回的類型非常具有迷惑性,我可不想讓你一開始就暈頭轉向。另外一處是最后的轉換,包含了move函數的本質。正如你所看到的,std::move接受了一個對象的引用做參數(準確的來說,應該是一個universal reference.請看Item 24。這個參數的格式是T&& param,但是請不要誤解為move接受的參數類型就是右值引用,請繼續往下看----菜b譯者注),并且返回指向同一個對象的引用。
函數返回值的"&&"部分表明std::move返回的是一個右值引用。但是呢,正如Item 28條解釋的那樣,如果T的類型恰好是一個左值引用,T&&的類型就會也會是左值引用。為了阻止這種事情的發生,我們用到了type trait(請看Item 9),在T上面應用std::remove_reference,它的效果就是“去除”T身上的引用,因此保證了"&&"應用到了一個非引用的類型上面。這就確保了std::move真正的返回的是一個右值引用(rvalue reference),這很重要,因為函數返回的rvalue reference就是右值(rvalue).因此,std::move就做了一件事情:將它的參數轉換成了右值(rvalue).
說一句題外話,std::move可以更優雅的在C++14中實現。感謝返回函數類型推導(function return type deduction 請看Item 3),感謝標準庫模板別名(alias template)`std::remove_reference_t`(請看Item 9),`std::move`可以這樣寫:
```cpp
template<typename T> //C++14; still in
decltype(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`做什么不做什么很重要。它只作轉換,不做move.
當然了,rvalues是對之執行move的合格候選者,所以對一個對象應用std::move告訴編譯器,該對象很合適對之執行move操作,所以std::move的名字就有意義了:標示出那些可以對之執行move的對象。
事實上,rvalues并不總是對之執行move的合格候選者。假設你正在寫一個類,它用來表示注釋。此類的構造函數接受一個包含注釋的std::string做參數,并且將此參數的值拷貝到一個數據成員上.受到Item 41的影響,你聲明一個接收by-value參數的構造函數:
```cpp
class Annotation{
public:
explicit Annotation(std::string text);//param to be copied,
... //so per Item 41, pass by value
};
```
但是Annotation的構造函數只需要讀取text的值。并不需要修改它。根據一個歷史悠久的傳統:能使用const的時候盡量使用。你修改了構造函數的聲明,text改為const:
```cpp
class Annotation{
public:
explicit Annotation(const std::string text);//param to be copied,
... //so per Item 41, pass by value
};
```
為了避免拷貝text到對象成員變量帶來拷貝代價。你繼續忠實于Item 41的建議,對text應用std::move,因此產生出一個rvalue:
```cpp
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;
};
```
這樣的代碼通過了編譯,鏈接,最后運行。而且把成員變量value設置成text的值。代碼跟你想象中的完美情況唯一不同的一點是,它沒有對text執行move到value,而是拷貝了text的值到value.text確實被std::move轉化成了rvalue,但是text被聲明為const std::string.所以在cast之前,text是一個const std::string類型的lvalue.cast的結果是一個const std::string的rvalue,但是自始至終,const的性質一直沒變。
代碼運行時,編譯器要選擇一個std::string的構造函數來調用。有以下兩種可能:
```cpp
class string{ //std::string is actually a
public: //typedef for std::basic_string<char>
...
string(const string& rhs); //copy ctor
string(string&& rhs); //move ctor
};
```
在Annotation的構造函數的成員初始化列表(member initialization list),`std::move(text)`的結果是const std::string的rvalue.這個rvalue不能傳遞給std::string的move構造函數,因為move構造函數接收的是非const的std::string的rvalue引用。然而,因為lvalue-reference-to-const的參數類型可以被const rvalue匹配上,所以rvalue可以被傳遞給拷貝構造函數.因此即使text被轉換成了rvalue,上文中的成員初始化仍調用了std::string的拷貝構造函數!這樣的行為對于保持const的正確性是必須的。從一個對象里move出一個值通常會改變這個對象,所以語言不允許將const對象傳遞給像move constructor這樣的會改變次對象的函數。
從本例中你可以學到兩點。首先,如果你想對這些對象執行move操作,就不要把它們聲明為const.對const對象的move請求通常會悄悄的執行到copy操作上。
std::forward的情況和std::move類似,但是和std::move__無條件地__將它的參數轉化為rvalue不同,std::forward在特定的條件下才會執行轉化。std::forward是一個__有條件__的轉化。為了理解它何時轉化何時不轉化,我們來回想一下std::forward的典型的使用場景。最常見的場景是:一個函數模板(function template)接受一個universal reference參數,將它傳遞給另外一個函數(作參數):
```cpp
void process(const Widget& lvalArg); //process lvalues
void process(Widget&& rvalArg); //process rvalues
template<typename T>
void logAndProcess(T&& param) //template that passes
//param to process
{
auto now = std::chrono::system_clock::now(); //get current time
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
```
請看下面對logAndProcess的兩個調用,一個使用的lvalue,另一個使用的rvalue:
```cpp
Widget w;
logAndProcess(w); //call with lvalue
logAndProcess(std::move(w)); //call with rvalue
```
在logAndProcess的實現中,參數param被傳遞給了函數process.process按照參數類型是lvalue或者rvalue都做了重載。當我們用lvalue調用logAndProcess時,我們自然地期望:forward給process的也是一個lvalue,當我們用rvalue來調用logAndProcess時,我們希望process的rvalue重載版本被調用。
但是就像所有函數的參數一樣,param可能是一個lvalue.logAndProcess內的每一個對process的調用因此想要調用process的lvalue重載版本。為了讓以上代碼的行為表現正確,我們需要一個機制,param轉化為rvalue當且僅當:傳遞給logAndProcess的用來初始化param的參數必須是一個rvalue.這正是std::forward做的事情。這就是為什么std::forward被稱作是一個__條件__轉化(conditional cast):當參數被rvalue初始化時,才將參數轉化為rvalue.
你可能想知道std::forward怎么知道它的參數是否被一個rvalue初始化。比如說,在以上的代碼中,std::forward怎么知道param被一個lvalue或者rvalue初始化?答案很簡單,這個信息蘊涵在logAndProcess的模板參數T中。這個參數傳遞給了std::forward,然后std::forward來從中解碼出此信息。欲知詳情,請參考Item 28。
std::move和std::forward都可以歸之為cast.唯一的一點不同是,std::move總是在執行casts,而std::forward是在某些條件滿足時才做。你可能覺得我們不用std::move,只使用std::forward會不會好一些。從一個純粹是技術的角度來說,答案是肯定的:std::forward是可以都做了,std::move不是必須的。當然,可以說這兩個函數都不是必須的,因為我們可以在任何地方都直接寫cast代碼,但是我希望我們在此達成共識:這樣做很惡心。
std::move的魅力在于:方便,減少了錯誤的概率,而且更加簡潔。舉個栗子,有這樣的一個class,我們想要跟蹤,它的move構造函數被調用了多少次,我們這次需要的是一個static的counter,它在每次move構造函數被調用時遞增。假設該class還有一個std::string類型的非靜態成員,下面是一個實現move constructor(使用std::move)的常見的例子:
```cpp
class Widget{
public:
Widget(Widget&& rhs)
: s(std::move(rhs.s))
{ ++moveCtorCalls; }
...
private:
static std::size_t moveCtorCalls;
std::string s;
}
```
如果要使用std::forward來實現同樣的行為,代碼像下面這樣寫:
```cpp
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的類型是非引用類型(non-reference),因為這就意味著傳遞的那個參數是一個rvalue(請看Item 28)。綜上,這就意味著std::move比std::forward用起來更方便(至少少敲了不少字),免去了讓我們傳遞一個表示函數參數是否是一個rvalue的類型參數。消除了傳遞錯誤類型(比如說,傳一個std::string&,可以導致數據成員s被拷貝構造,而不是想要的move構造)的可能性。
更重要的是,std::move的使用表明了對rvalue的無條件的轉換,然而,當std::forward只對被綁定了rvalue的reference進行轉換。這是兩個非常不同的行為。std::move就是為了move操作而生,而std::forward,就是將一個對象轉發(或者說傳遞)給另外一個函數,同時保留此對象的左值性或右值性(lvalueness or rvalueness)。所以我們需要這兩個不同的函數(并且是不同的函數名字)來區分這兩個操作。
|要記住的東西|
|:--------- |
|std::move執行一個無條件的對rvalue的轉化。對于它自己本身來說,它不會move任何東西|
|std::forward在參數被綁定為rvalue的情況下才會將它轉化為rvalue|
|std::move和std::forward在runtime時啥都不做|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款1:理解模板類型推導
- 條款2:理解auto類型推導
- 條款3:理解decltype
- 條款4:知道如何查看類型推導
- 第二章 auto關鍵字
- 條款5:優先使用auto而非顯式類型聲明
- 條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
- 第三章 使用現代C++
- 條款7:創建對象時區分()和{}
- 條款8:優先使用nullptr而不是0或者NULL
- 條款9:優先使用聲明別名而不是typedef
- 條款10:優先使用作用域限制的enmu而不是無作用域的enum
- 條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
- 條款12:使用override關鍵字聲明覆蓋的函數
- 條款13:優先使用const_iterator而不是iterator
- 條款14:使用noexcept修飾不想拋出異常的函數
- 條款15:盡可能的使用constexpr
- 條款16:保證const成員函數線程安全
- 條款17:理解特殊成員函數的生成
- 第四章 智能指針
- 條款18:使用std::unique_ptr管理獨占資源
- 條款19:使用std::shared_ptr管理共享資源
- 條款20:在std::shared_ptr類似指針可以懸掛時使用std::weak_ptr
- 條款21:優先使用std::make_unique和std::make_shared而不是直接使用new
- 條款22:當使用Pimpl的時候在實現文件中定義特殊的成員函數
- 第五章 右值引用、移動語義和完美轉發
- 條款23:理解std::move和std::forward
- 條款24:區分通用引用和右值引用
- 條款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 條款26:避免在通用引用上重定義函數
- 條款27:熟悉通用引用上重定義函數的其他選擇
- 條款28:理解引用折疊
- 條款29:假定移動操作不存在,不廉價,不使用
- 條款30:熟悉完美轉發和失敗的情況
- 第六章 Lambda表達式
- 條款31:避免默認的參數捕捉
- 條款32:使用init捕捉來移動對象到閉包
- 條款33:在auto&&參數上使用decltype當std::forward auto&&參數
- 條款34:優先使用lambda而不是std::bind
- 第七章 并發API
- 條款35:優先使用task-based而不是thread-based
- 條款36:當異步是必要的時聲明std::launch::async
- 條款37:使得std::thread在所有的路徑下無法join
- 條款38:注意線程句柄析構的行為
- 條款39:考慮在一次性事件通信上void的特性
- 條款40:在并發時使用std::atomic 在特殊內存上使用volatile
- 第八章 改進
- 條款41:考慮對拷貝參數按值傳遞移動廉價,那就盡量拷貝
- 條款42:考慮使用emplace代替insert