右值、右值引用(T&&)、std::move、完美轉發(std::forward)、通用引用(universal reference)、引用折疊(reference collapsing)這些概念貌似很玄,但其實很它們想做的事情很簡單而且很單一,抓住本質就豁然開朗了。
上面這些概念的引入完全是為了解決一個問題:盡可能用淺拷貝代替深拷貝;具體該如何實施呢:
1. 定義move constructor或move operator=,如果你新定義的類沒有move constructor或move operator=,那根本就用不上上面那些高級的概念;
2. 告訴編譯器什么時候用move constructor(而不是用copy constructor),這就需要借助std::move來把左值轉成右值;
下面是一些零散的箴言:
左值:能用&來取地址的東西(有名字的都是左值,不管其是否是const的,而且引用都是左值,因為引用肯定得起個名字去引用另外一個東西);
右值:不能用&來取地址的東西(說白了就是沒名字的東西,例如函數的返回值);
const T&是萬能引用,即 const T& t = XXX; //XXX是左值、右值都可以;
T& t = XXX; //XXX必須是左值;
T&& t = XXX; //XXX必須是右值;
const T&& t = XXX; //XXX必須是右值;
不要把T&&和const T&劃等號,兩個都能hold住臨時變量,給臨時變量續命,但:
T&& t1 = get_val();
const T& t2 = get_val();
t1和t2都是左值,因為都可以用&來取地址,但t1是非const的,因此可以t1.xxx = yyy;去修改t1里的內容(當然如果你定義const T&& t1 = get_val(),那就不能更改了),這樣一來,下面這種代碼受益了:
string name = get_name(); //因為后面要修改name,所以不能用const string& name
trim(name);
可以修改為:
string&& name = get_name();
trim(name);
關于右值引用的生命周期(續命能續多久,下面討論的也適用于const T&):
1. 函數不能返回對局部變量的引用(右值引用也不行),這是鐵的原則;
2. 引用一旦建立(用一個名字hold住了臨時變量),例如T&& t = get_val();那么這個臨時變量的析構時機就是變量t退出其作用域的時候,因此下面的代碼是錯誤的:
struct A {
A(T&& t): _t(t) {};
T&& _t;
};
A* pa = NULL;
{
T&& t = get_val();
pa = new A(t);
}
pa->_t; //runtime crash, _t已經析構了
std::move只做一件事:把左值轉成右值,因此T t(std::move(get_val()));不管什么情況下都能保證調用move constructor來構造t;
c++0x里stl的各種容器都已經新增了move constructor,因此當你希望使用淺拷貝的時候可以借助std::move來達成所愿了,至于什么時候可以用淺拷貝,分兩種情況,有些情況編譯器會默認幫你去調用move constructor,但在你自己新定義的函數或者類成員方法里就需要你自己來判斷了,大的原則就是:這個變量只是傳遞過去就好,中間可以轉好幾道手,但在傳遞過程中不需要做修改;
完美轉發(std::forward),談到這個就必須得談到通用引用(universal reference)、引用折疊(reference collapsing),其實這幾個概念是捆綁在一起的,而且只用在模板范疇內,而且只是為了解決下面這一種模式:
template <typename T>
void func2(T t) {
}
template <typename T>
void func(T&& t) {
func2(std::forward<T>(t));
}
上面這個到底完美在哪里?
func(get_val()); //這個會導致最后是通過move constructor來構造func2函數里的參數t
T t0 = get_val();
T& t1 = t0;
const T& t2 = get_val();
T&& t3 = get_val();
const T&& t4 = get_val();
func(t0); func(t1); func(t2); func(t3); func(t4); //這5個都會導致最后是通過copy constructor來構造func2函數里的參數t,因為這個5個t*都是左值(有名字的就是左值);
插播兩個你有可能費解的地方:
1. func函數的參數是T&&, t0、t1都是左值,不是說不能用左值賦值給右值嗎?這就要提到引用折疊了(詳情可參見最后我推薦的文章),說白了因為func是個模板函數才能這么干;
2. t3類型是T&&,func的參數也是T&&,把t3傳遞過去,居然還是調用copy contructor,因為t3是右值引用,它引用了一個右值,但它本身卻是左值,到了func函數內部,func函數只能知道它是個左值,了解不到它原本的面貌居然是個右值引用;如果還不理解,再看下下面:
template <typename T>
void print(T t) {
}
int x1 = 1; int& x2 = x1; const int& x3 = x1; int* x4 = &x1;
分別用x{1..4}來調用print方法,你肯定知道x1、x2、x3到了print函數里,T就是int,只有x4會被print函數認為T是int*,引用這個概念在模板類型推導時是無法讓模板參數感知到的;此外print(T)和print(T&)是不能同定義的,編譯器會抱怨ambiguous,這個事實也能幫你多一些理解。
如果想讓上面的這5個最終能通過move constructor來構造func2函數里的參數t,那么只要給每個都用func(std::move(t*))包裝下就可以了;
再澄清下,func2函數的參數是類型T,并不是引用,因此無論如何都需要生成T的一個新實例,區別就是到底是通過copy constructor還是move constructor來生成了;
所以,所謂的完美就是體現在了forward能把本來是左值的按左值來傳遞,本來是右值的按照右值來傳遞,具體來說就是作為中間環節的func函數內部實現過程中使用了func2(std::forward<T>(t));這句話,使得可以按照調用方實際的真實情況告知給func2函數如何構造參數t,這有什么好處呢,還是那句話:在適當的時機做合適的引導,讓編譯器幫你調用淺拷貝。
除此之外,你完全可以忘掉這些玄乎其神的概念,所以只要會套用就可以了。
以上是一些梗概性或結論性的東西,再結合下面這篇文章,把骨頭之外的血肉補上吧:
https://codinfox.github.io/dev/2014/06/03/move-semantic-perfect-forward/