C++11除了帶來了右值引用以外,還引入了一種稱為“萬能引用”的語法;通過“萬能引用”,對某型別的引用T&&,既可以表達右值引用,也可以表達左值引用。
定義
該語法有兩種使用場景,最常見的一種是作為函數模板的形參:
template<typename T>
void f(T&& param);
其中param就是一個萬能引用。
第二個場景則是auto聲明:
auto&& var2 = var1;
這兩種情況都涉及到了型別的推導,也就是說,如果你雖然遇到了T&&的形式,但是不涉及型別推導,那么它只是一個右值引用:
void f(Widget&& param);
同時,想要成為萬能引用,變量聲明的形式也必須正確無誤,必須正好是形如“T&&”才行。比如下面這些情況就不是萬能引用:
template<typename T>
void f1(std::vector<T>&& param); // param是一個右值引用
template<typename T>
void f2(const T&& param); //param也是一個右值引用
如果你向f1和f2傳左值,會都是會直接報錯的。
除此之外,像下面這種情況也不是萬能引用:
template<class T, class Allocator=allocator<T>>
class vector{
public:
void push_back(T&& x);
};
push_back的形參x的型別T是受vector影響的,假設給定T為Widget,那么就會被實例化為如下代碼:
template<class Widget, class Allocator=allocator<T>>
class vector{
public:
void push_back(Widget&& x);
};
而作為對比,vector類中的emplace_back函數則是一個實實在在的萬能引用:
template<class T, class Allocator=allocator<T>>
class vector{
public:
template<class...Args>
void emplace_back(Args&&.. args);
};
args的型別Args完全獨立於vector的型別形參T,Args必須在每次調用emplace_back時被推導,因此args是萬能引用。
為什么是“萬能引用”?
你一定非常好奇,為什么這種形態被稱作“萬能引用”。原因正像前文所說的,通過“萬能引用”,對某型別的引用T&&,既可以表達右值引用,也可以表達左值引用。
比如說,傳入一個右值引用,一般都要給傳入的參數加一個std::move操作確保變量的可移動性:
class Widget
{
public:
Widget(Widget&& rhs):name(std::move(rhs.name)){}
private:
std::string name;
};
而萬能引用則有所不同,它一般是通過std::forward來進行轉換:
class Widget
{
public:
template<typename T>
void setName(T&& newName)
{name = std::forward<T>(newName);}
private:
std::string name;
};
在這種情況下,若newName是一個左值引用,則forward函數不會對它進行操作,它的返回值仍然是一個左值引用;若newName是一個右值,則會進行std::move的轉換。用戶使用時無需區分,這也正是它“萬能”之處。
舉個例子:
Widget w;
std::string c = "123";
w.setName(w); //傳入是一個左值,forward返回左值引用
w.setName("123"); //傳入是一個右值,forward返回右值引用
w.setName(std::move(w)); //傳入是一個右值,forward返回右值引用
引用折疊
你一定會很奇怪,為什么萬能引用的形式明明是T&&,卻既可以代表左值又可以代表右值。這就要涉及到C++的引用折疊語法了。
首先,C++不支持“引用的引用”這種概念,這樣的代碼在C++中是非法的:
int x;
auto& & rx = x;
但是,假設我們向前面的萬能引用函數f傳入一個左值引用:
Widget w = w;
f(w); //T的推導型別為Widget&
那么實例化的結果如下:
void f(Widget& && param);
之所以這樣的代碼能通過,是因為在特殊的情況下(比如模板實例化),C++應用了引用折疊的語法。
有左值和右值兩種引用,所以就有四種可能的組合:左值-左值、左值-右值、右值-左值、右值-右值,如果引用的引用出現在允許的語境,改雙重引用會被折疊成單個引用:
如果任一引用為左值引用,則結果為左值引用。否則(即兩個都為右值引用),結果為右值引用。
因此上述例子中,最終將param推導為左值引用。
此外,auto的型別推導也會應用引用折疊的場景,例如:
Widget w;
auto &&w1 =w; //w1是個左值
話說到這里,我們就可以更深入地理解萬能引用,其實它就是滿足了下面兩個條件的語境中的右值引用:
1.型別推導的過程會區別左值和右值。T型別的左值推導結果為T&,而T型別的右值推導結果為T。
2.會發生引用折疊。