參考文章:ios
刷 Leetcode 時,時不時遇到以下 2 種遍歷 STL 容器的寫法:git
int main() { vector<int> v = {1, 2, 3, 4}; for (auto &x: v) cout<<x<<' '; cout<<endl; for (auto &&x: v) cout<<x<<' '; cout<<endl; }
一個困擾我好久的問題是 auto &
和 auto &&
有什麼區別?github
首先要明確一個概念,值 (Value) 和變量 (Variable) 並非同一個東西:ide
i + j + k
)。左值(lvalue, left value),顧名思義就是賦值符號左邊的值。準確來講, 左值是表達式(不必定是賦值表達式)後依然存在的持久對象。函數
右值(rvalue, right value),右邊的值,是指表達式結束後就再也不存在的臨時對象。性能
C++11 中爲了引入強大的右值引用,將右值的概念進行了進一步的劃分,分爲:純右值和將亡值。優化
純右值 (prvalue, pure rvalue),純粹的右值,要麼是純粹的字面量,例如
10, true
; 要麼是求值結果至關於字面量或匿名臨時對象,例如1+2
。非引用返回的臨時變量、運算表達式產生的臨時變量、原始字面量、Lambda 表達式都屬於純右值。this
C++( 包括 C ) 中全部的表達式和變量要麼是左值,要麼是右值。通俗的左值的定義就是非臨時對象,那些能夠在多條語句中使用的對象。全部的變量都知足這個定義,在多條代碼中均可以使用,都是左值。右值是指臨時的對象,它們只在當前的語句中有效。spa
例子:翻譯
int i = 0; // ok, i is lvalue, 0 is rval // 右值也能夠出如今賦值表達式的左邊, 可是不能做爲賦值的對象,由於右值只在當前語句有效,賦值沒有意義。 // 0 做爲右值出如今了」=」的左邊。可是賦值對象是 i 或者 j,都是左值。 (i > 0? i : j) = 233
總結:
須要注意的是,字符串字面量只有在類中才是右值,當其位於普通函數中是左值。例如:
class Foo { const char *&&right = "this is a rvalue"; // 此處字符串字面量爲右值 // const char *&right = "hello world"; // error public: void bar() { right = "still rvalue"; // 此處字符串字面量爲右值 } }; int main() { const char *const &left = "this is an lvalue"; // 此處字符串字面量爲左值 // left = "123"; // error }
將亡值 (xvalue, expiring value),是 C++11 爲了引入右值引用而提出的概念 (所以在傳統 C++ 中,純右值和右值是同一個概念),也就是即將被銷燬、卻可以被移動的值。將亡值表達式,即:
move
先看一個例子:
vector<int> foo() { vector<int> v = {1,2,3,4,5}; return v; } auto v1 = foo();
按照傳統 C++ 的方式(也是咱們這些 C++ 菜鳥的理解),上述代碼的執行方式爲:foo()
在函數內部建立並返回一個臨時對象 v
,而後執行 vector<int>
的拷貝構造函數,完成 v1
的初始化,最後對 foo
內的臨時對象進行銷燬。
那麼,在某一時刻,就存在 2 份相同的 vector
數據。若是這個對象很大,就會形成大量額外的開銷。
在 v1 = foo()
中,v1
是一個左值,能夠被繼續使用,但foo()
就是一個純右值, foo()
產生的那個返回值做爲一個臨時值,一 旦被 v1
複製後,將當即被銷燬,沒法獲取、也不能修改。
而將亡值就定義了這樣一種行爲: 臨時的值可以被識別、同時又可以被移動。
在 C++11 以後,編譯器爲咱們作了一些工做,foo()
內部的左值 v
會被進行隱式右值轉換,等價於 static_cast<vector<int> &&>(v)
,進而此處的 v1
會將 foo
局部返回的值進行移動。也就是後面將會提到的移動語義 std::move()
。
我的的理解是,這種語法的引入是爲了實現與 Java 中相似的對象引用系統。
先看一段代碼:
int a; a = 2; //a是左值,2是右值 a = 3; //左值能夠被更改,編譯經過 2 = 3; //右值不能被更改,錯誤 int b = 3; int* pb = &b; //pb是左值,&b是右值,由於它是由取址運算符返回的值 &b = 0; //錯誤,右值不能被更改 // lvalues: int i = 42; i = 43; // ok, i is an lvalue int* p = &i; // ok, i is an lvalue int& foo(); foo() = 42; // ok, foo() is an lvalue int* p1 = &foo(); // ok, foo() is an lvalue // rvalues: int foobar(); int j = 0; j = foobar(); // ok, foobar() is an rvalue int k = j + 2; // ok, j+2 is an rvalue int* p2 = &foobar(); // error, cannot take the address of an rvalue j = 42; // ok, 42 is an rvalue
那麼問題來了:函數返回值是否只會是右值?固然不是。
vector<int> v(10, 0); v[0] = 111;
顯然,v[0]
會執行 []
的符號重載函數 int& operator[](const int x)
, 所以函數的返回值也是可能爲左值的。
要拿到一個將亡值,就須要用到右值引用 T &&
,其中 T
是類型。右值引用的聲明讓這個臨時值的生命週期得以延長,只要變量還活着,那麼將亡值將繼續存活。
C++11 提供了 std::move
這個方法將左值參數無條件的轉換爲右值,有了它咱們就可以方便的得到一個右值臨時對象,例如:
#include <iostream> #include <string> using namespace std; void reference(string &str) { cout << "lvalue ref" << endl; } void reference(string &&str) { cout << "rvalue ref" << endl; } int main() { string lv1 = "string,"; // lv1 is lvalue // string &&r1 = lv1; // 非法,右值引用不能引用左值 string &&rv1 = std::move(lv1); // 合法,move 可將左值轉移爲右值 cout << rv1 << endl; // string &lv2 = lv1 + lv1; // 非法,很是量引用的初始值必須爲左值 const string &lv2 = lv1 + lv1; // 合法,常量左值引用可以延長臨時變量的生命週期 cout << lv2 << endl; string &&rv2 = lv1 + lv2; // 合法,右值引用延長臨時對象生命週期(經過 rvalue reference 引用 rval) rv2 += "Test"; cout << rv2 << endl; reference(rv2); // 輸出 "lvalue ref" // rv2 雖然引用了一個右值,但因爲它是一個引用,因此 rv2 依然是一個左值。 // 也就是說,T&& Doesn’t Always Mean 「Rvalue Reference」, 它既能夠綁定左值,也能綁定右值 }
爲何不容許很是量引用綁定到左值?
一種解釋以下(C++ 真傻逼)。
這個問題至關於解釋下面一段代碼:
int i = 233; int &r0 = i; // ok double &r1 = i; // error const double &r3 = i; // ok
由於 double &r1
類型與 int i
不匹配,因此不行,那爲何 const double &r3 = i
是能夠的?由於它實際上至關於:
const double t = (double)i; const double &r3 = t;
在 C++ 中,全部的臨時變量都是 const
類型的,因此沒有 const
就不行。
先看一段代碼,熟悉一下 move
作了些什麼:
#include <iostream> #include <string> using namespace std; int main() { string a = "sinkinben"; string b = move(a); cout << "a = \"" << a << "\"" << endl; cout << "b = \"" << b << "\"" << endl; } // Output // a = "" // b = "sinkinben"
而後看完下面一段代碼,結束這一回合。
template <class T> swap(T& a, T& b){ T tmp(a); //現有兩份a的拷貝,tmp和a a = b; //現有兩份b的拷貝,a和b b = tmp; //現有兩份tmp的拷貝,b和tmp } //試試更好的方法,不會生成額外的拷貝 template <class T> swap(T& a, T& b){ T tmp(std::move(a)); //只有一份拷貝,tmp a = std::move(b); //只有一份拷貝,a b = std::move(tmp); //只有一份拷貝,b }
我的感受,b = move(a)
這一語義操做,是把變量 b
綁定到數據 a
的內存區域上,從而避免了無心義的數據拷貝操做。
下面這一段代碼能夠印證個人這個觀點。
#include <iostream> class A { public: int *pointer; A() : pointer(new int(1)) { std::cout << "構造" << pointer << std::endl; } A(A &a) : pointer(new int(*a.pointer)) { std::cout << "拷貝" << pointer << std::endl; } // 無心義的對象拷貝 A(A &&a) : pointer(a.pointer) { a.pointer = nullptr; std::cout << "移動" << pointer << std::endl; } ~A() { std::cout << "析構" << pointer << std::endl; delete pointer; } }; // 防止編譯器優化 A return_rvalue(bool test) { A a, b; if (test) return a; // 等價於 static_cast<A&&>(a); else return b; // 等價於 static_cast<A&&>(b); } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; } /* Output 構造0x7f8477405800 構造0x7f8477405810 移動0x7f8477405810 析構0x0 析構0x7f8477405800 obj: 0x7f8477405810 1 析構0x7f8477405810 */
對於 queue
或者 vector
,咱們也能夠經過 move
提升性能:
// q is a queue auto x = std::move(q.front()); q.pop(); // v is a vertor v.push_back(std::move(x));
若是 STL 中的元素「體積」都很大,這麼作也能節省一點開銷,提升性能。
恕我直言,這個翻譯是個辣雞。英文名叫 Perfect Forwarding .
這是爲了解決這樣一個問題:實參被傳入到函數中,當它被再傳到另外一個函數中,它依然是一個左值或右值。
template <class T> void f2(T t){ cout<<"f2"<<endl; } template <class T> void f1(T t){ cout<<"f1"<<endl; f2(t); //若是t是右值,咱們但願傳入f2也是右值;若是t是左值,咱們但願傳入f2也是左值 } //在main函數裏: int a = 2; f1(3); //傳入右值 f1(a); //傳入左值
在引進👆巴拉巴拉的這一套機制以前,即 C++11以前的狀況是怎麼樣的呢?當咱們從 f1
調用 f2
的時候,無論傳入 f1
的是右值仍是左值,由於 t
是一個變量名,傳入 f2
的時候都變成了左值,這就會形成由於調用 T
的拷貝構造函數而生成沒必要要的拷貝浪費大量資源。
那麼如今有一個叫 forward
的函數,就能夠這樣作:
template <class T> void f2(T t){ cout<<"f2"<<endl; } template <class T> void f1(T&& t) { //這是通用引用,而不是右值引用 cout<"f1"<<endl; f2(std::forward<T>(t)); //std::forward<T>(t)用來把t轉發爲左值或右值,決定於T }
這樣,f1
調用 f2
的時候,調用的就是移動構造函數而不是拷貝構造函數,能夠避免沒必要要的拷貝,這就叫「完美轉發」。
完美轉發,傻逼到家。
本文開始提出的問題 auto &
和 auto &&
有什麼區別?這個問題就更復雜了,涉及到 Universal Reference 這個概念,能夠參考這 2 篇文章:
有空再說。
傻逼 C++ 。