(一)什么是左值和右值?
????????傳統的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現在開始我們 之前學習的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。
?1.左值
????????左值是一個表示數據的表達式(如變量名或解引用的指針),我們可以獲取它的地址+可以對它賦值,左值可以出現賦值符號的左邊,右值不能出現在賦值符號左邊。定義時const修飾符后的左 值,不能給他賦值,但是可以取它的地址。左值引用就是給左值的引用,給左值取別名。
?特別注意:
? ? ? ? const修飾后的左值,不能給他賦值,但它依然是左值。
int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下幾個是對上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}
?2.右值
????????右值也是一個表示數據的表達式,如:字面常量、表達式返回值,函數返回值(這個不能是左值引 用返回)等等,右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊,右值不能取地址。右值引用就是對右值的引用,給右值取別名。
注意:
? ? ? ?引用右值是“&&“, 右值的引用是左值(例如下面中的rr1,rr2,rr3都是左值)。
double fmin(double x, double y)
{return x > y ? y : x;
}int main()
{double x = 1.1, y = 2.2;// 以下幾個都是常見的右值10;x + y;fmin(x, y);// 以下幾個都是對右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y); //fmin函數會返回一個臨時變量,這個變量是右值// 這里編譯會報錯:error C2106: “=”: 左操作數必須為左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;
}
需要注意的是右值是不能取地址的,但是給右值取別名后,會導致右值被存儲到特定位置,且可 以取到該位置的地址,也就是說例如:不能取字面量10的地址,但是rr1引用后,可以對rr1取地 址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感覺很神奇, 這個了解一下實際中右值引用的使用場景并不在于此,這個特性也不重要。 ?
int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5; ?// 報錯return 0;
}
?(二)左值引用與右值引用比較
1.左值引用:
????????1. 左值引用只能引用左值,不能引用右值。
????????2. 但是const左值引用既可引用左值,也可引用右值。
int main() {// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; ? // ra為a的別名//int& ra2 = 10; ? // 編譯失敗,因為10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0; }
2.右值引用:?
????????1. 右值引用只能右值,不能引用左值。
????????2. 但是右值引用可以move以后的左值。
int main() {// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 無法從“int”轉換為“int &&”// message : 無法將左值綁定到右值引用int a = 10;int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);//move(a)之后,a并不是轉換成了右值,a仍然為左值//(簡單的)可以理解為調用move函數,move返回了一個和a數據一摸一樣的右值。return 0;}
注意:
? ? ? ? move(a)并不是將a變成了左值,(可以這么理解)而是該函數返回了一個與a數據一樣匿名對象,但是對該右值進行操作會影響到a。
(三)?右值引用使用場景和意義
左值引用的短板:
????????當函數返回對象是一個局部變量,出了函數作用域就不存在了,就不能使用左值引用返回, 只能傳值返回。????????
????????例如下面的:bit::string to_string(int value)函數中可以看到,這里只能使用傳值返回, 傳值返回會導致至少1次拷貝構造(如果是一些舊一點的編譯器可能是兩次拷貝構造)。
????????
????????新的編譯器會對上面的構造進行優化(出現連續的構造函數時,編譯器會進行優化)
移動語義? ?
????????移動構造本質是將參數右值的資源竊取過來,占位已有,那么就不用做深拷貝了,所以它叫做移動構造,就是竊取別人的資源來構造自己。
?
????????這里to_string函數生成了一個str,這個str是左值,正常來說,to_string返回時,會再次調用構造函數生成一個臨時對象,但是這里編譯器做了優化(str被識別成將亡值,to_string返回時編譯器會將str識別成右值,進行返回),所以ret2的初始化操作省去了生成匿名對象,只有一個str的深拷貝構造和一個移動語義。
? ? ? ? 簡單來說,就是將str當成了右值進行ret2的初始化操作,所以只有一個移動語義
?
不僅僅有移動構造,還有移動賦值:
// 移動賦值 string& operator=(string&& s) {cout << "string& operator=(string&& s) -- 移動語義" << endl;swap(s);return *this; } int main() {bit::string ret1;ret1 = bit::to_string(1234);return 0; } // 運行結果: // string(string&& s) -- 移動語義 // string& operator=(string&& s) -- 移動語義
????????這里運行后,我們看到調用了一次移動構造和一次移動賦值。
????????因為如果是用一個已經存在的對象接收,編譯器就沒辦法優化了。bit::to_string函數中會先用str生成構造生成一個臨時對象,但是 我們可以看到,編譯器很聰明的在這里把str識別成了右值,調用了移動構造。然后在把這個臨時 對象做為bit::to_string函數調用的返回值賦值給ret1,這里調用的移動賦值。
? ? ? ? 簡單來說,這里將str識別成右值,然后調用移動構造生成臨時對象進行賦值操作,所以這里有一個一次移動構造和一次移動賦值
?
Move()函數?
????????按照語法,右值引用只能引用右值,但右值引用一定不能引用左值嗎?因為:有些場景下,可能 真的需要用右值去引用左值實現移動語義。當需要用右值引用引用一個左值時,可以通過move 函數將左值轉化為右值。C++11中,std::move()函數位于 頭文件中,該函數名字具有迷惑性, 它并不搬移任何東西,唯一的功能就是將一個左值強制轉化為右值引用,然后實現移動語義。
? ?如下面例子:
? ? ?
int main() {bit::string s1("hello world");// 這里s1是左值,調用的是拷貝構造bit::string s2(s1);// 這里我們把s1 move處理以后, 會被當成右值,調用移動構造// 但是這里要注意,一般是不要這樣用的,因為我們會發現s1的// 資源被轉移給了s3,s1被置空了。bit::string s3(std::move(s1));return 0; }
需要注意的是:
? ? ? ? move函數將s1轉化為右值(返回一個s1的右值),但是s1本身還是左值。
還有下面這個例子:
?
?
(四)完美轉發
1.萬能引用(引用折疊)
?????????模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力。
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } // 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。 // 模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力, // 但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值, // 我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學習的完美轉發 template<typename T> void PerfectForward(T&& t) {Fun(t); } int main() {PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0; }
運行結果:
可以看到不管是左值還是右值,引用后的對象都為左值(即 t 是左值);
?
如果我們保留原來的屬性呢??
? ? ? ? 因此有了std::forward 完美轉發在傳參的過程中保留對象原生類型屬性
2.完美轉發實際中的使用場景:
簡單舉個例子(該例子存在問題,只是幫助理解使用場景):
void Insert(Node* pos, T&& x); void Insert(Node* pos, const T& x);template<class T> void PushFront(T&& x) {//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x)); }int main() {PushFront("2222");return 0; }
這里我們調用PushFront(),即可以傳左值,也可以傳右值,因為有完美轉發,和forward()。
?