條款23提起過把一個實參傳遞給模板函數時,無論實參是左值還是右值,推斷出來的模板參數都會含有編碼信息。那條款沒有提起,只有模板形參是通用引用時,這件事才會發生,不過對於這疏忽,理由很充分:條款24才介紹通用引用。把這些關於通用引用和左值/右值編碼信息綜合,意味著這個模板,
templatevoid func(T&& param);
無論param是個左值還是右值,需要推斷的模板參數T都會被編碼。
編碼技術是很簡單的,當傳遞的實參是個左值時,T就被推斷為一個左值引用,當傳遞的實參是個右值時,T就被推斷為一個非引用類型。(注意這是不對稱的:左值會編碼為左值引用,但右值編碼為非引用。)因此:
Widget widgetFactory(); // 返回右值的函數
Widget w; // 一個變量,左值
func(w); // 用左值調用函數,T被推斷為Widget&
func(widgetFactory()); // 用右值調用函數,T被推斷為Widget
兩個func調用都是用Widget參數,不過一個Widget是左值,另一個是右值,從而導致了模板參數T被推斷出不同的類型。這,正如我們將很快看到,是什麼決定通用引用變成左值引用或右值引用的,而這也是std::forward完成工作所使用的內部技術。
在我們緊密關注std::forward和通用引用之前,我們必須注意到,在C++中對引用進行引用是不合法的。你可以嘗試聲明一個,你的編譯器會嚴厲譴責加抗議:
int x;
...
auto& & rx = x; // 報錯!不可以聲明對引用的引用
但想一想當一個左值被傳遞給接受通用引用的模板函數時:
template
void func(T&& param); // 如前
func(w); // 用左值調用func,T被推斷為Widget&
如果使用推斷出來的T類型(即Widget&)實例化模板,我們得到這個:
void func(Widget& && param);
一個對引用的引用!然而你的編譯器內有深刻譴責加抗議。我們從條款24知道,通用引用
param用一個左值進行初始化,
param的類型應該出一個左值引用,但編譯器是如何推斷T的類型的,還有是怎樣把它替代成下面這個樣子,哪一個才是最終的簽名呢?
void func(Widget& param);
答案是引用折疊。是的,你是禁止聲明對引用的引用,但編譯器在特殊的上下文中可以產生它們,模板實例化就是其中之一。當編譯器生成對引用的引用時,引用折疊指令就會隨後執行。
有兩種類型的引用(左值和右值),所以有4種可能的對引用引用的組合(左值對左值,左值對右值,右值對左值,右值對右值)。如果對引用的引用出現在被允許的上下文(例如,在模板實例化時),這個引用(即引用的引用,兩個引用)會折疊成一個引用,根據的是下面的規則:
如果兩個引用中有一個是左值引用,那麼折疊的結果是一個左值引用。否則(即兩個都是右值引用),折疊的結果是一個右值引用。
在我們上面的例子中,在函數func中把推斷出來的類型Widget&替代T後,產生了一個對右值的左值引用,然後引用折疊規則告訴我們結果是個左值引用。
引用折疊是使std::forward工作的關鍵部分。就如條款25解釋那樣,對通用引用使用std::forward,是一種常見的情況,像這樣:
template
void f(T&& fParam)
{
... // do some works
someFunc(std::forward(fParam)); // 把fParam轉發到someFunc
}
因為
fParam是一個通用引用,我們知道無論傳遞給函數f的實參(即用來初始化fParam的表達式)是左值還是右值,參數類型T都會被編碼。std::forward的工作是,當且僅當傳遞給函數f的實參是個右值時,把
fParam(左值)轉換成一個右值。
這裡是如何實現std::forward來完成工作:
template // 在命名空間std中
T&& forward(typename remove_reference::type& param)
{
return static_cast(param);
}
這沒有完全順應標准庫(我省略了一些接口細節),不過不同的部分是與理解std::forward如何工作無關。
假如傳遞給函數f的是個左值的Widget,T會被推斷為Widget&,然後調用std::forward會讓它實例化為
std::forward
Widget& && forward(typename remove_reference::type& param)
{
return static_cast(param);
}
remove_reference
Widget& && forward(Widget& param)
{ return static_cast(param); }
在返回類型和cast中都會發生引用折疊,導致被調用的最終版本的std::forward:
Widget& forward(Widget& param)
{ return static_cast(param); }
就如你所見,當一個左值被傳遞給模板函數f時,std::forward被實例化為接受一個左值引用和返回一個左值引用。std::forward內部的顯式轉換沒有做任何東西,因為
param的類型已經是Widget&了,所以這次轉換沒造成任何影響。一個左值實參被傳遞給std::forward,將會返回一個左值引用。根據定義,左值引用是左值,所以傳遞一個左值給std::forward,會導致std::forward返回一個左值,就跟它應該做的那樣。
現在假設傳遞給函數f的是個右值的Widget。在這種情況下,函數f的類型參數T會被推斷為Widget。因此f裡面的std::forward會變成
std::forward
Widget&& forward(typename remove_reference::type& param)
{
return static_cast(param);
}
對非引用Widget使用std::remove_reference會產生原來的類型(Widget),所以std::forward變成這樣:
Widget&& forward(Widget& param)
{ return static_cast(param); }
這裡沒有對引用的引用,所以沒有進行引用折疊,這也就這次std::forward調用的最終實例化版本。
由函數返回的右值引用被定義為右值,所以在這種情況下,std::forward會把f的參數
fParam(一個左值)轉換成一個右值。最終結果是傳遞給函數f的右值實參作為右值被轉發到someFunc函數,這是順理成章的事情。
在C++14中,std::remove_reference_t的存在可以讓std::forward的實現變得更簡潔:
template // C++14,在命名空間std中
T&& forward(remove_reference_t& param)
{
return static_cast(param);
}
引用折疊出現在四種上下文。第一種是最常見的,就是模板實例化。第二種是auto變量的類型生成。它的細節本質上和模板實例化相同,因為auto變量的類型推斷和模板類型推斷本質上相同(看條款2)。再次看回之前的一個例子:
template
void func(T&& param);
Widget widgetFactory(); // 返回右值的函數
Widget w; // 一個變量,左值
func(w); // 用左值調用函數,T被推斷為Widget&
func(widgetFactory()); // 用右值調用函數,T被推斷為Widget
這可以用auto形式模仿。這聲明
auto&& w1 = w;
用個左值初始化w1,因此auto被推斷為Widget&。在聲明中用Widget&代替auto聲明w1,產生這個對引用進行引用的代碼,
Widget& && w1 = w;
這在引用折疊之後,變成
Widget& w1 = w;
結果是,w1是個左值引用。
另一方面,這個聲明
auto&&w2 = widgetFactory();
用個右值初始化w2,導致auto被推斷為無引用類型Widget,然後用Widget替代auto變成這樣:
Widget&& w2 = widgetFactory();
這裡沒有對引用的引用,所以我們已經完成了,w2是個右值引用。
我們現在處於真正能理解條款24介紹通用引用的位置了。通用引用不是一種新的引用類型,實際上它是右值引用——在滿足了下面兩個條件的上下文中:
根據左值和右值來進行類型推斷。T類型的左值使T被推斷為T&,T類型的右值使T被推斷為T。 發生引用折疊
通用引用的概念是很有用的,因為它讓你免受:識別出存在引用折疊的上下文,弱智地根據左值和右值推斷上下文,然後弱智地把推斷出的類型代進上下文,最後使用引用折疊規則。
我說過有4中這樣的上下文,不過我們只討論了兩種:模板實例化和auto類型生成。第三種上下文就是使用typedef和類型別名聲明(看條款9)。如果,在typedef創建或者評估期間,出現了對引用的引用,引用折疊會出面消除它們。例如,假如我們有個類模板Widget,內部嵌有一個右值引用類型的typedef,
template
class Widget {
public:
typedef T&& RvalueRefToT;
...
};
然後假如我們用一個左值引用來實例化Widget:
Widget w;
在Widget中用int&代替T,typedef變成這樣:
typedef int& && RvalueRefToT;
引用折疊把代碼弄出這樣:
type int& RvalueRefToT;
這很明顯的告訴我們,我們typedef選擇的名字跟我們期望得到的不一樣:當用左值引用實例化Widget時,RvalueRefToT是個左值引用的typedef。
最後的一種會發生引用折疊的上下文是使用decltype中。如果,在分析一個使用decltype的類型期間,出現了對引用的引用,引用折疊會出面消除它。(關於decltype的詳細信息,請看條款3。)
總結
需要記住的3點:
引用折疊會出現在4中上下文:模板實例化,auto類型生成,typedef和類型別名聲明的創建和使用,decltype。 當編譯器在一個引用折疊上下文中生成了對引用的引用時,結果會變成一個引用。如果原來的引用中有一個是左值引用,結果就是個左值引用。否則,結果是個右值引用。 通用引用是——出現在類型推斷區分左值和右值和出現引用折疊的上下文中的——右值引用。