#Item 24: Distinguish universal references from rvalue references.
大多數人說真相可以讓我們感到自由,但是在某些情況下,一個巧妙的謊言也可以讓人覺得非常輕松。這個Item就是要編制一個“謊言”。因為我們是在和軟件打交道。所以我們避開“謊言”這個詞:我們是在編制一種“抽象”的意境。
為了聲明一個類型T的右值引用,你寫下了T&&。下面的假設看起來合理:你在代碼中看到了一個"T&&"時,你看到的就是一個右值引用。但是,它可沒有想象中那么簡單:
```cpp
void f(Widget&& param); //rvalue reference
Widget&& var1 = Widget(); //rvalue reference
auto&& var2 = var1; //not rvalue reference
template<typename T>
void f(std::vector<T>&& param) //rvalue reference
template<typename T>
void f(T&& param); //not rvalue reference
```
實際上,“T&&”有兩個不同的意思。首先,當然是作為rvalue reference,這樣的引用表現起來和你預期一致:只和rvalue做綁定,它們存在的意義就是表示出可以從中move from的對象。
“T&&”的另外一個含義是:既可以是rvalue reference也可以是lvalue reference。這樣的references在代碼中看起來像是rvalue reference(即"T&&"),但是它們_也_可以表現得就像他們是lvalue refernces(即"T&")那樣.它們的dual nature允許他們既可以綁定在rvalues(like rvalue references)也可以綁定在lvalues(like lvalue references)上。進一步來說,它們可以綁定到const或者non-const,volatile或者non-volatile,甚至是const + volatile對象上面。它們幾乎可以綁定到任何東西上面。為了對得起它的全能,我決定給它們起個名字:universal reference.(Item25將會解釋universal references總是可以將std::forward應用在它們之上,本書出版之時,C++委員會的一些人開始將universal references稱之為forward references).
兩種上下文中會出現universal references。最普通的一種是function template parameters,就像上面的代碼所描述的例子那樣:
```cpp
template<typename T>
void f(T&& param); //param is a universal reference
```
第二種context就是auto的聲明方式,如下所示:
```cpp
auto&& var2 = var1; //var2 is a universal reference
```
這兩種context的共同點是:都有type deduction的存在。在template fucntion f中,參數param的類型是被deduce出來的,在var2的聲明中,var2的類型也是被deduce出來的。和接下來的例子(也可以和上面的栗子一塊兒比)對比我們會發現,下面栗子是不存在type deduction的。如果你看到"T&&",卻沒有看到type deduction.那么你看到的就是一個rvalue reference:
```cpp
void f(Widget&& param); //no type deduction
//param is an rvalue reference
Widget&& var1 = Widget(); //no type deduction
//var1 is an rvalue reference
```
因為universal references是references,所以它們必須被初始化。universal reference的initializer決定了它表達的是rvalue reference或者lvalue reference。如果initializer是rvalue,那么universal reference對應的是rvalue reference.如果initializer是lvalue,那么universal reference對應的就是lvalue reference.對于身為函數參數的universal reference,initializer在call site(調用處)被提供:
```cpp
template<typename T>
void f(T&& param); //param is a universal reference
Widget w;
f(w); //lvalue passed to f;param's type is Widget&(i.e., an lvalue reference)
f(std::move(w)); //rvalue passed to f;param's type is Widget&&(i.e., an rvalue reference)
```
對universal的reference來說,type deduction是必須的,但還是不夠,它要求的格式也很嚴格,必須是"T&&".再看下我們之前寫過的栗子:
```cpp
template<typename T>
void f(std::vector<T>&& param); //param is an rvalue reference
```
當f被調用時,類型T會被deduce(除非調用者顯式的指明類型,這種邊緣情況我們不予考慮)。param聲明的格式不是T&&,而是std::vector<T>&&.這就說明它不是universal reference,而是一個rvalue reference.如果你傳一個lvalue給f,那么編譯器肯定就不高興了。
```cpp
std::vector<int> v;
f(v); //error! can't bind lvalue to rvalue reference
```
即使一個最簡單前綴const.也可以把一個reference成為universal reference的可能抹殺:
```cpp
template<typename T>
void f(const T&& param); //param is an rvalue reference
```
如果你在一個template里面,并且看到了T&&這樣的格式,你可能就會假設它就是一個universal reference.但是并非如此,因為還差一個必要的條件:type deduction.在template里面可不保證一定有type deduction.看個例子,std::vector里面的push_back方法。
```cpp
template<class T, class Allocator = allocator<T>>
class vector{
public:
void push_back(T&& x);
...
}
```
以上便是只有T&&格式卻沒有type deduction的例子,push_back的存在依賴于一個被instantiation的vector.用于instantiation的type就完全決定了push_back的函數聲明。也就是說
```cpp
std::vector<Widget> v;
```
使得std::vector的template被instantiated成為如下格式:
```cpp
class vector<Widget, allocator<Widget>>{
public:
void push_back(Widget&& x); //rvalue reference
...
};
```
如你所見,push_back沒有用到type deduction.所以這個vector<T>的$push_back$(有兩個overload的$push_back$)所接受的參數類型是rvalue-reference-to-T.
與之相反,std::vector中概念上相近的$emplace_back$函數確實用到了type deduction:
```cpp
template<class T,class Allocator=allocator<T>>
class vector{
public:
template <class... Args>
void emplace_back(Args&&... args);
...
};
```
type parameter```Args```獨立于vector的type parameter ```T```,所以每次調用```emplace_back```的時候,```Args```就要被deduce一次。(實際上,```Args```是一個parameter pack.并不是type parameter.但是為了討論的方便,我們姑且稱之為type parameter)。
我之前說universal reference的格式必須是```T&&```, 事實上,emplace_back的type parameter名字命名為Args,但這不影響args是一個universal reference,管它叫做T還是叫做Args呢,沒啥區別。舉個例子,下面的template接受的參數就是universal reference.一是因為格式是"type&&",二是因為param的type會被deduce(再一次提一下,除非caller顯示的指明了type這種邊角情況).
```cpp
template<typename MyTemplateType> //param is a
void someFunc(MyTemplateType&& param); //universal reference
```
我之前提到過auto變量可以是universal references.更準確的說,聲明為auto&&的變量就是universal references.因為類型推導發生并且它們也有正確的格式("T&&").auto類型的universal references并不想上面說的那種用來做function template parameters的universal references那么常見,在最近的C++ 11和C++ 14中,它們變得非常活躍。C++ 14中的lambda expression允許聲明auto&&的parameters.舉個栗子,如果你想寫一個C++ 14的lambda來記錄任意函數調用花費的時間,你可以這么寫:
```cpp
auto timeFuncInvocation =
[](auto&& func, auto&&... params) //C++ 14
{
start timer;
std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)... //invoke func on params
);
stop timer and record elapsed time
}
```
如果你對于"std::forward<decltype(blah blah blah)"的代碼的反應是“這是什么鬼!?”,這只是意味著你還沒看過Item33,不要為此擔心。
func是一個可以綁定到任何callable object(lvaue或者rvalue)的universal reference.args是0個或多個universal references(即a universal reference parameter pack),它可以綁定到任意type,任意數量的objects.所以,多虧了auto universal reference,timeFuncInvocation可以記錄pretty much any的函數調用(any和pretty much any的區別,請看Item 30).
本Item中universal reference的基礎,其實是一個謊言,呃,或者說是一種"abstraction".隱藏的事實被稱之為_reference collapsing_,Item 28會講述。但是該事實并不會降低它的用途。rvalue references以及universal references會幫助你更準確第閱讀source code(“Does that T&& I’m looking at bind to rvalues only or to everything?”),和同事溝通時避免歧義(“I’m using a universal reference here, not an rvalue reference...”),它也會使得你理解Item 25和Item 26,這兩條都依賴于此區別。所以,接受理解這個abstraction吧。牛頓三大定律(abstraction)比愛因斯坦的相對論(truth)一樣有用且更容易應用,universal reference的概念也可以是你理解reference collapsing 的細節。
|要記住的東西|
|:--------- |
|如果一個函數的template parameter有著T&&的格式,且有一個deduce type T.或者一個對象被生命為auto&&,那么這個parameter或者object就是一個universal reference.|
|如果type的聲明的格式不完全是type&&,或者type deduction沒有發生,那么type&&表示的是一個rvalue reference.|
|universal reference如果被rvalue初始化,它就是rvalue reference.如果被lvalue初始化,他就是lvaue reference.|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款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