C++11新特性之右值引用與完美轉(zhuǎn)發(fā)詳解
一、左值與右值
顧名思義,左值就是只能放在等號左邊的值,右值是只能放在等號右邊的值。
在C++Prime一書中,對左值和右值的劃分為,左值是一個表示數(shù)據(jù)的表達式,右值是一個即將銷毀的值(通常稱為將亡值)。比如我們定義的一個變量就是一個左值,而字面常量,表達式返回值,傳值返回函數(shù)的返回值就是右值。
10;//右值 int a = 10;//a是左值 add(2, 3);//右值 x+y;//右值 const int a;//左值
注意,const類型的變量是不能放在等號左側(cè)來為它賦值的,但是他是一個左值。
這里給出一個區(qū)分兩者的方式:可以取地址的就是左值,不能取地址的就是右值!
二、左值引用與右值引用
我們之前所寫的引用都是左值引用符號是&,左值引用的底層是使用指針,它的作用是為對象取一個別名。
而右值引用就是給右值取別名,它的符號是&&,右值引用開辟了空間,得到的一個對象是左值。
int a = 10; int& d = a;//左值引用 int&& e = 10;//右值引用 int&& f = a + 1; int&& c = add(2, 3);
左值引用不能給右值取別名,右值引用也不能給左值取別名。但是如果對左值進行move(),對左值引用加上const是可以這樣進行的。
move的意思就是保證除了賦值和銷毀之外,不再使用該左值,即將a的屬性轉(zhuǎn)移到了e中,對左值move后是一共右值。
int&& c = a;//右值引用不能給左值取別名 int& d = add(3, 4);//左值引用不能給右值取別名 int&& e = move(a);//當對左值加move的時候可以 const int& f = add(3, 4);//當對引用加const后可以取別名
同時右值引用不像左值引用一樣具有傳遞性:
int&& a = 10; a=20; cout<<&a<<endl; //int&& b = a;//錯誤
這是因為a是一個左值,我們可以打印a的地址,右值經(jīng)過引用后得到的對象是一個左值。因此我們是可以對a進行賦值的。
三、右值引用應(yīng)用
1.移動構(gòu)造與移動賦值
移動構(gòu)造與移動賦值在C++11中已經(jīng)加入了STL容器的函數(shù)中:
string(string&& str) //移動構(gòu)造
string& operator=(string&& str)//移動賦值
移動構(gòu)造與移動賦值都是向函數(shù)中傳入右值引用,它們的本質(zhì)與右值基本相同,就是將一個將亡值的數(shù)據(jù)轉(zhuǎn)移給另一個值。
我們可以在函數(shù)string中模擬實現(xiàn)一下移動構(gòu)造和移動賦值,它們的本質(zhì)就是調(diào)用swap函數(shù)完成賦值,而不是使用strcpy創(chuàng)建一個新對象。
1.模擬實現(xiàn)的string
為了方便觀察,我們使用自己模擬實現(xiàn)的string來進行說明:
namespace my_string { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } //構(gòu)造函數(shù) string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } // s1.swap(s2) void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 拷貝構(gòu)造 string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 深拷貝" << endl; string tmp(s._str); swap(tmp); } // 移動構(gòu)造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 資源轉(zhuǎn)移" << endl; this->swap(s); } // 移動賦值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 轉(zhuǎn)移資源" << endl; swap(s); return *this; } //賦值 string& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷貝" << endl; string tmp(s); swap(tmp); return *this; } ~string() { delete[] _str; _str = nullptr; } //下標訪問 char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } //調(diào)換順序 void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } //插入 void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } //string operator+=(char ch) string& operator+=(char ch) { push_back(ch); return *this; } string operator+(char ch) { string tmp(*this); push_back(ch); return tmp; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做標識的\0 }; my_string::string to_string(int value) { my_string::string str; while (value) { int val = value % 10; str += ('0' + val); value /= 10; } reverse(str.begin(), str.end()); return str; } }
2.移動構(gòu)造
當我們調(diào)用to_string的時候:
my_string::string ret = my_string::to_string(1234);
當我們不添加移動構(gòu)造的時候,可以發(fā)現(xiàn)最終進行的是一次深拷貝和一次淺拷貝:
這里發(fā)現(xiàn)只調(diào)用了一次拷貝構(gòu)造,這是因為編譯器做了優(yōu)化,如果不優(yōu)化的話,str拷貝構(gòu)造臨時對象,然后臨時對象作為to_string的返回值再拷貝構(gòu)造給ret。其實是發(fā)生了兩次拷貝構(gòu)造。
但是編譯器做了優(yōu)化之后,在to_string函數(shù)快結(jié)束時,返回前直接用str構(gòu)造ret。
當我們加入拷貝構(gòu)造之后,會發(fā)現(xiàn)只發(fā)生了一次移動構(gòu)造就可以了:
其實在這一過程中編譯器也做了優(yōu)化,str先拷貝構(gòu)造形成一個臨時對象,再由臨時對象進行移動構(gòu)造賦值給ret。
編譯器做了優(yōu)化之后,將str直接當成左值(相當于move了一下),然后進行移動構(gòu)造生成ret。
通過觀察打印結(jié)果可以發(fā)現(xiàn),顯然移動構(gòu)造沒有再開辟空間,而是直接將數(shù)據(jù)進行轉(zhuǎn)移,節(jié)省了空間,由臨時變量進行拷貝構(gòu)造給ret還會創(chuàng)建一個新的對象,消耗空間。
3.移動賦值
my_string::string ret; ret = my_string::to_string(1234);//調(diào)用移動賦值
當不使用移動賦值的時候,以上代碼是兩段深拷貝實現(xiàn)的:
首先str會調(diào)用移動構(gòu)造,生成臨時對象,然后臨時對象再調(diào)用賦值拷貝構(gòu)造(深拷貝),定義ret。
當引入移動賦值之后,這個過程就變成了str調(diào)用移動構(gòu)造生成臨時對象,臨時對象再通過移動運算符重載生成ret,整個過程中沒有一次深拷貝。
C++11中,所有STL容器中,都會提供一個右值引用的版本。
四、默認移動構(gòu)造和移動賦值重載函數(shù)
與六大成員函數(shù)一樣,編譯器在一定的條件下,也會生成自己的默認移動構(gòu)造函數(shù),只不過生成的條件更加復(fù)雜:
1.如果你自己沒有實現(xiàn)移動構(gòu)造函數(shù),并且沒有實現(xiàn)析構(gòu)函數(shù),拷貝構(gòu)造,拷貝賦值構(gòu)造中的任意一個。那么編譯器會自動生成一個默認構(gòu)造函數(shù)。默認生成的移動構(gòu)造函數(shù),對內(nèi)置類型進行直接拷貝,對于自定義類型,如果有對應(yīng)的移動構(gòu)造函數(shù)就調(diào)用其對應(yīng)的移動構(gòu)造函數(shù),如果沒有那么調(diào)用拷貝構(gòu)造。
2.如果你沒自己實現(xiàn)移動賦值重載函數(shù),且沒有實現(xiàn)析構(gòu)函數(shù),拷貝構(gòu)造,拷貝賦值重載中的任何一個,編譯器會自動生成一個移動賦值重載函數(shù)。默認生成的移動賦值重載函數(shù),對內(nèi)置類型直接進行賦值,對于自定義類型,如果有對應(yīng)的移動賦值重載函數(shù)就調(diào)用其對應(yīng)的移動賦值重載函數(shù),如果沒有則調(diào)用拷貝賦值。
3.如果你提供了移動賦值構(gòu)造或者移動賦值重載函數(shù),那么編譯器就不會自動生成。
五、完美轉(zhuǎn)發(fā)
1.萬能引用
在模板中,&&表示的不是右值引用,而是萬能引用,即既可以接收左值,又可以接收右值。
void PerfectForward(T&& t) { Fun(forward<T>(t)); }
此時傳入的t既可以是左值,也可以是右值。
2.完美轉(zhuǎn)發(fā)
運行以下程序,發(fā)現(xiàn)最終識別的都是左值引用。
void Func(int&& x) { cout << "rvalue" << endl; } void Func(int& x) { cout << "lvalue" << endl; } template<class T> void PerfectForward(T&& t) { Func(t); } int main() { PerfectForward(10);//左值 int a; PerfectForward(a);//左值 PerfectForward(move(a));//左值 }
這是因為右值引用一旦引用了,就變成了左值,如果我們還希望保持該右值引用的特性的話,需要使用forward函數(shù)來對其進行封裝:
Func(forward<T>(t));
forward(t)來進行封裝的意義在于,保持t原來的屬性,如果它原來是左值那么封裝之后還是左值,如果它是右值的引用,則將其還原成右值。該函數(shù)的作用稱為完美轉(zhuǎn)發(fā),由于這一性質(zhì),STL容器的插入也可以使用右值引用來實現(xiàn)。
即支持:
vector<int> v;v.push_back(111);
在該右值引用版本的插入中,調(diào)用的就是forward(val)。
到此這篇關(guān)于C++11新特性之右值引用與完美轉(zhuǎn)發(fā)詳解的文章就介紹到這了,更多相關(guān)C++右值引用 完美轉(zhuǎn)發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++用一棵紅黑樹同時封裝出set與map的實現(xiàn)代碼
set中存儲的一般為鍵K即可,而map存儲的一般都是鍵值對KV,也就是說他們結(jié)構(gòu)是不同的,那么我們?nèi)绾尾拍苡靡活w紅黑樹同時封裝出set與map兩種容器呢,那么接下來我們具體地來研究下STL庫中是怎樣實現(xiàn)的,并且進行模擬實現(xiàn),需要的朋友可以參考下2024-03-03