C++11中的右值引用
http://www.cnblogs.com/yanqi0124/p/4723698.html
在C++98中有左值和右值的概念,不過這兩個概念對于很多程序員并不關心,因為不知道這兩個概念照樣可以寫出好程序。在C++11中對右值的概念進行了增強,我個人理解這部分內(nèi)容是C++11引入的特性中最難以理解的了。該特性的引入至少可以解決C++98中的移動語義和完美轉發(fā)問題,若你還不清楚這兩個問題是什么,請向下看。
溫馨提示,由于內(nèi)容比較難懂,請仔細看。C++已經(jīng)夠復雜了,C++11中引入的新特性令C++更加復雜了。在學習本文的時候一定要理解清楚左值、右值、左值引用和右值引用。
移動構造函數(shù)
首先看一個C++98中的關于函數(shù)返回類對象的例子。
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566 | class MyString {public: MyString() { _data = nullptr; _len = 0; printf("Constructor is called!\n");} MyString(const char* p) { _len = strlen (p); _init_data(p); cout << "Constructor is called! this->_data: " << (long)_data << endl;} MyString(const MyString& str) { _len = str._len; _init_data(str._data); cout << "Copy Constructor is called! src: " << (long)str._data << " dst: " << (long)_data << endl;} ~MyString() { if (_data){ cout << "DeConstructor is called! this->_data: " << (long)_data << endl; free(_data);} else{ std::cout << "DeConstructor is called!" << std::endl; }} MyString& operator=(const MyString& str) { if (this != &str) { _len = str._len; _init_data(str._data); } cout << "Copy Assignment is called! src: " << (long)str._data << " dst" << (long)_data << endl; return *this; } operator const char *() const { return _data;}private: char *_data; size_t _len; void _init_data(const char *s) { _data = new char[_len+1]; memcpy(_data, s, _len); _data[_len] = '\0'; } }; MyString foo(){ MyString middle("123"); return middle;}int main() { MyString a = foo(); return 1;} |
該例子在編譯器沒有進行優(yōu)化的情況下會輸出以下內(nèi)容,我在輸出的內(nèi)容中做了注釋處理,如果連這個例子的輸出都看不懂,建議再看一下C++的語法了。我這里使用的編譯器命令為g++ test.cpp -o main -g -fno-elide-constructors,之所以要加上-fno-elide-constructors選項時因為g++編譯器默認情況下會對函數(shù)返回類對象的情況作返回值優(yōu)化處理,這不是我們討論的重點。
| 123456 | Constructor is called! this->_data: 29483024 // middle對象的構造函數(shù)Copy Constructor is called! src: 29483024 dst: 29483056 // 臨時對象的構造,通過middle對象調(diào)用復制構造函數(shù)DeConstructor is called! this->_data: 29483024 // middle對象的析構Copy Constructor is called! src: 29483056 dst: 29483024 // a對象構造,通過臨時對象調(diào)用復制構造函數(shù)DeConstructor is called! this->_data: 29483056 // 臨時對象析構DeConstructor is called! this->_data: 29483024 // a對象析構 |
在上述例子中,臨時對象的構造、復制和析構操作所帶來的效率影響一直是C++中為人詬病的問題,臨時對象的構造和析構操作均對堆上的內(nèi)存進行操作,而如果_data的內(nèi)存過大,勢必會非常影響效率。從程序員的角度而言,該臨時對象是透明的。而這一問題正是C++11中需要解決的問題。
在C++11中解決該問題的思路為,引入了移動構造函數(shù),移動構造函數(shù)的定義如下。
| 123456 | MyString(MyString &&str) { cout << "Move Constructor is called! src: " << (long)str._data << endl;_len = str._len;_data = str._data;str._data = nullptr;} |
在移動構造函數(shù)中我們竊取了str對象已經(jīng)申請的內(nèi)存,將其拿為己用,并將str申請的內(nèi)存給賦值為nullptr。移動構造函數(shù)和復制構造函數(shù)的不同之處在于移動構造函數(shù)的參數(shù)使用&&,這就是下文要講解的右值引用符號。參數(shù)不再是const,因為在移動構造函數(shù)需要修改右值str的內(nèi)容。
移動構造函數(shù)的調(diào)用時機為用來構造臨時變量和用臨時變量來構造對象的時候移動語義會被調(diào)用。可以通過下面的輸出結果看到,我們所使用的編譯參數(shù)為g++ test.cpp -o main -g -fno-elide-constructors --std=c++11。
| 123456 | Constructor is called! this->_data: 22872080 // middle對象構造Move Constructor is called! src: 22872080 // 臨時對象通過移動構造函數(shù)構造,將middle申請的內(nèi)存竊取DeConstructor is called! // middle對象析構Move Constructor is called! src: 22872080 // 對象a通過移動構造函數(shù)構造,將臨時對象的內(nèi)存竊取DeConstructor is called! // 臨時對象析構DeConstructor is called! this->_data: 22872080 // 對象a析構 |
通過輸出結果可以看出,整個過程中僅申請了一塊內(nèi)存,這也正好符合我們的要求了。
C++98中的左值和右值
我們先來看下C++98中的左值和右值的概念。左值和右值最直觀的理解就是一條語句等號左邊的為左值,等號右邊的為右值,而事實上該種理解是錯誤的。左值:可以取地址,有名字的值,是一個指向某內(nèi)存空間的表達式,可以使用&操作符獲取內(nèi)存地址。右值:不能取地址,即非左值的都是右值,沒有名字的值,是一個臨時值,表達式結束后右值就沒有意義了。我想通過下面的例子,讀者可以清楚的理解左值和右值了。
| 12345678910111213141516 | // lvalues://int i = 42;i = 43; // i是左值int* p = &i; // i是左值int& foo();foo() = 42; // foo()返回引用類型是左值int* p1 = &foo(); // foo()可以取地址是左值// rvalues://int foobar();int j = 0;j = foobar(); // foobar()是右值int* p2 = &foobar(); // 編譯錯誤,foobar()是右值不能取地址j = 42; // 42是右值 |
C++11右值引用和移動語義
在C++98中有引用的概念,對于const int &m = 1,其中m為引用類型,可以對其取地址,故為左值。在C++11中,引入了右值引用的概念,使用&&來表示。在引入了右值引用后,在函數(shù)重載時可以根據(jù)是左值引用還是右值引用來區(qū)分。
| 12345678910111213141516 | void fun(MyString &str){ cout << "left reference" << endl;}void fun(MyString &&str){ cout << "right reference" << endl;}int main() { MyString a("456"); fun(a); // 左值引用,調(diào)用void fun(MyString &str)fun(foo()); // 右值引用,調(diào)用void fun(MyString &&str) return 1;} |
在絕大多數(shù)情況下,這種通過左值引用和右值引用重載函數(shù)的方式僅會在類的構造函數(shù)和賦值操作符中出現(xiàn),被例子僅是為了方便采用函數(shù)的形式,該種形式的函數(shù)用到的比較少。上述代碼中所使用的將資源從一個對象到另外一個對象之間的轉移就是移動語義。這里提到的資源是指類中的在堆上申請的內(nèi)存、文件描述符等資源。
前面已經(jīng)介紹過了移動構造函數(shù)的具體形式和使用情況,這里對移動賦值操作符的定義再說明一下,并將main函數(shù)的內(nèi)容也一起更改,將得到如下輸出結果。
| 1234567891011121314151617181920212223242526272829 | MyString& operator=(MyString&& str) { cout << "Move Operator= is called! src: " << (long)str._data << endl; if (this != &str) { if (_data != nullptr){ free(_data);}_len = str._len;_data = str._data;str._len = 0;str._data = nullptr;} return *this; }int main() { MyString b;b = foo(); return 1;}// 輸出結果,整個過程僅申請了一個內(nèi)存地址Constructor is called! // 對象b構造函數(shù)調(diào)用Constructor is called! this->_data: 14835728 // middle對象構造Move Constructor is called! src: 14835728 // 臨時對象通過移動構造函數(shù)由middle對象構造DeConstructor is called! // middle對象析構Move Operator= is called! src: 14835728 // 對象b通過移動賦值操作符由臨時對象賦值DeConstructor is called! // 臨時對象析構DeConstructor is called! this->_data: 14835728 // 對象b析構函數(shù)調(diào)用 |
在C++中對一個變量可以通過const來修飾,而const和引用是對變量約束的兩種方式,為并行存在,相互獨立。因此,就可以劃分為了const左值引用、非const左值引用、const右值引用和非const右值引用四種類型。其中左值引用的綁定規(guī)則和C++98中是一致的。
非const左值引用只能綁定到非const左值,不能綁定到const右值、非const右值和const左值。這一點可以通過const關鍵字的語義來判斷。
const左值引用可以綁定到任何類型,包括const左值、非const左值、const右值和非const右值,屬于萬能引用類型。其中綁定const右值的規(guī)則比較少見,但是語法上是可行的,比如const int &a = 1,只是我們一般都會直接使用int &a = 1了。
非const右值引用不能綁定到任何左值和const右值,只能綁定非const右值。
const右值引用類型僅是為了語法的完整性而設計的, 比如可以使用const MyString &&right_ref = foo(),但是右值引用類型的引入主要是為了移動語義,而移動語義需要右值引用是可以被修改的,因此const右值引用類型沒有實際意義。
我們通過表格的形式對上文中提到的四種引用類型可以綁定的類型進行總結。
| ? |
非const左值引用 | 是 | 否 | 否 | 否 |無 |
const左值引用 | 是 | 是 | 是 | 是 | 全能綁定類型,綁定到const右值的情況比較少見 |
非const右值引用 | 否 | 否 | 是 | 否 | C++11中引入的特性,用于移動語義和完美轉發(fā) |
const值引用 | 是 | 否 | 否 | 否 | 沒有實際意義,為了語法完整性而存在 |
下面針對上述例子,我們看一下foo函數(shù)綁定參數(shù)的情況。
如果只實現(xiàn)了void foo(MyString &str),而沒有實現(xiàn)void fun(MyString &&str),則和之前一樣foo函數(shù)的實參只能是非const左值。
如果只實現(xiàn)了void foo(const MyString &str),而沒有實現(xiàn)void fun(MyString &&str),則和之前一樣foo函數(shù)的參數(shù)即可以是左值又可以是右值,因為const左值引用是萬能綁定類型。
如果只實現(xiàn)了void foo(MyString &&str),而沒有實現(xiàn)void fun(MyString &str),則foo函數(shù)的參數(shù)只能是非const右值。
強制移動語義std::move()
前文中我們通過右值引用給類增加移動構造函數(shù)和移動賦值操作符已經(jīng)解決了函數(shù)返回類對象效率低下的問題。那么還有什么問題沒有解決呢?
在C++98中的swap函數(shù)的實現(xiàn)形式如下,在該函數(shù)中我們可以看到整個函數(shù)中的變量a、b、c均為左值,無法直接使用前面移動語義。
| 1234567 | template <class T> void swap ( T& a, T& b ){ T c(a); a=b;b=c;} |
但是如果該函數(shù)中能夠使用移動語義是非常合適的,僅是為了交換兩個變量,卻要反復申請和釋放資源。按照前面的知識變量c不可能為非const右值引用,因為變量a為非const左值,非const右值引用不能綁定到任何左值。
在C++11的標準庫中引入了std::move()函數(shù)來解決該問題,該函數(shù)的作用為將其參數(shù)轉換為右值。在C++11中的swap函數(shù)就可以更改為了:
| 1234567 | template <class T> void swap (T& a, T& b){T c(std::move(a)); a=std::move(b); b=std::move(c);} |
在使用了move語義以后,swap函數(shù)的效率會大大提升,我們更改main函數(shù)后測試如下:
| 1234567891011121314151617 | int main() { // move函數(shù) MyString d("123"); MyString e("456");swap(d, e); return 1;}// 輸出結果,通過輸出結果可以看出對象交換是成功的Constructor is called! this->_data: 38469648 // 對象d構造Constructor is called! this->_data: 38469680 // 對象e構造Move Constructor is called! src: 38469648 // swap函數(shù)中的對象c通過移動構造函數(shù)構造Move Operator= is called! src: 38469680 // swap函數(shù)中的對象a通過移動賦值操作符賦值Move Operator= is called! src: 38469648 // swap函數(shù)中的對象b通過移動賦值操作符賦值DeConstructor is called! // swap函數(shù)中的對象c析構DeConstructor is called! this->_data: 38469648 // 對象e析構DeConstructor is called! this->_data: 38469680 // 對象d析構 |
右值引用和右值的關系
這個問題就有點繞了,需要開動思考一下右值引用和右值是啥含義了。讀者會憑空的認為右值引用肯定是右值,其實不然。我們在之前的例子中添加如下代碼,并將main函數(shù)進行修改如下:
| 123456789101112131415161718192021 | void test_rvalue_rref(MyString &&str){ cout << "tmp object construct start" << endl;MyString tmp = str; cout << "tmp object construct finish" << endl;}int main() {test_rvalue_rref(foo()); return 1;}// 輸出結果Constructor is called! this->_data: 28913680Move Constructor is called! src: 28913680DeConstructor is called!tmp object construct startCopy Constructor is called! src: 28913680 dst: 28913712 // 可以看到這里調(diào)用的是復制構造函數(shù)而不是移動構造函數(shù)tmp object construct finishDeConstructor is called! this->_data: 28913712DeConstructor is called! this->_data: 28913680 |
我想程序運行的結果肯定跟大多數(shù)人想到的不一樣,“Are you kidding me?不是應該調(diào)用移動構造函數(shù)嗎?為什么調(diào)用了復制構造函數(shù)?”。關于右值引用和左右值之間的規(guī)則是:
如果右值引用有名字則為左值,如果右值引用沒有名字則為右值。
通過規(guī)則我們可以發(fā)現(xiàn),在我們的例子中右值引用str是有名字的,因此為左值,tmp的構造會調(diào)用復制構造函數(shù)。之所以會這樣,是因為如果tmp構造的時候調(diào)用了移動構造函數(shù),則調(diào)用完成后str的申請的內(nèi)存自己已經(jīng)不可用了,如果在該函數(shù)中該語句的后面在調(diào)用str變量會出現(xiàn)我們意想不到的問題。鑒于此,我們也就能夠理解為什么有名字的右值引用是左值了。如果已經(jīng)確定在tmp構造語句的后面不需要使用str變量了,可以使用std::move()函數(shù)將str變量從左值轉換為右值,這樣tmp變量的構造就可以使用移動構造函數(shù)了。
而如果我們調(diào)用的是MyString b = foo()語句,由于foo()函數(shù)返回的是臨時對象沒有名字屬于右值,因此b的構造會調(diào)用移動構造函數(shù)。
該規(guī)則非常的重要,要想能夠正確使用右值引用,該規(guī)則必須要掌握,否則寫出來的代碼會有一個大坑。
完美轉發(fā)
前面已經(jīng)介紹了本文的兩大主題之一的移動語義,還剩下完美轉發(fā)機制。完美轉發(fā)機制通常用于庫函數(shù)中,至少在我的工作中還是很少使用的。如果實在不想理解該問題,可以不用向下看了。在泛型編程中,經(jīng)常會遇到的一個問題是怎樣將一組參數(shù)原封不動的轉發(fā)給另外一個函數(shù)。這里的原封不動是指,如果函數(shù)是左值,那么轉發(fā)給的那個函數(shù)也要接收一個左值;如果參數(shù)是右值,那么轉發(fā)給的函數(shù)也要接收一個右值;如果參數(shù)是const的,轉發(fā)給的函數(shù)也要接收一個const參數(shù);如果參數(shù)是非const的,轉發(fā)給的函數(shù)也要接收一個非const值。
該問題看上去非常簡單,其實不然。看一個例子:
| 1234567891011121314151617181920212223242526 | using namespace std;void fun(int &) { cout << "lvalue ref" << endl; } void fun(int &&) { cout << "rvalue ref" << endl; } void fun(const int &) { cout << "const lvalue ref" << endl; } void fun(const int &&) { cout << "const rvalue ref" << endl; }template<typename T>void PerfectForward(T t) { fun(t); } int main(){PerfectForward(10); // rvalue ref int a;PerfectForward(a); // lvalue refPerfectForward(std::move(a)); // rvalue ref const int b = 8;PerfectForward(b); // const lvalue refPerfectForward(std::move(b)); // const rvalue ref return 0;} |
在上述例子中,我們想達到的目的是PerfectForward模板函數(shù)能夠完美轉發(fā)參數(shù)t到fun函數(shù)中。上述例子中的PerfectForward函數(shù)必然不能夠達到此目的,因為PerfectForward函數(shù)的參數(shù)為左值類型,調(diào)用的fun函數(shù)也必然為void fun(int &)。且調(diào)用PerfectForward之前就產(chǎn)生了一次參數(shù)的復制操作,因此這樣的轉發(fā)只能稱之為正確轉發(fā),而不是完美轉發(fā)。要想達到完美轉發(fā),需要做到像轉發(fā)函數(shù)不存在一樣的效率。
因此,我們考慮將PerfectForward函數(shù)的參數(shù)更改為引用類型,因為引用類型不會有額外的開銷。另外,還需要考慮轉發(fā)函數(shù)PerfectForward是否可以接收引用類型。如果轉發(fā)函數(shù)PerfectForward僅能接收左值引用或右值引用的一種,那么也無法實現(xiàn)完美轉發(fā)。
我們考慮使用const T &t類型的參數(shù),因為我們在前文中提到過,const左值引用類型可以綁定到任何類型。但是這樣目標函數(shù)就不一定能接收const左值引用類型的參數(shù)了。const左值引用屬于左值,非const左值引用和非const右值引用是無法綁定到const左值的。
如果將參數(shù)t更改為非const右值引用、const右值也是不可以實現(xiàn)完美轉發(fā)的。
在C++11中為了能夠解決完美轉發(fā)問題,引入了更為復雜的規(guī)則:引用折疊規(guī)則和特殊模板參數(shù)推導規(guī)則。
引用折疊推導規(guī)則
為了能夠理解清楚引用折疊規(guī)則,還是通過以下例子來學習。
| 12345678910 | typedef int& TR;int main(){ int a = 1; int &b = a; int & &c = a; // 編譯器報錯,不可以對引用再顯示添加引用TR &d = a; // 通過typedef定義的類型隱式添加引用是可以的 return 1;} |
在C++中,不可以在程序中對引用再顯示添加引用類型,對于int & &c的聲明變量方式,編譯器會提示錯誤。但是如果在上下文中(包括使用模板實例化、typedef、auto類型推斷等)出現(xiàn)了對引用類型再添加引用的情況,編譯器是可以編譯通過的。具體的引用折疊規(guī)則如下,可以看出一旦引用中定義了左值類型,折疊規(guī)則總是將其折疊為左值引用。這就是引用折疊規(guī)則的全部內(nèi)容了。另外折疊規(guī)則跟變量的const特性是沒有關系的。
| 1234 | A& & => A&A& && => A&A&& & => A&A&& && => A&& |
特殊模板參數(shù)推導規(guī)則
下面我們再來學習特殊模板參數(shù)推導規(guī)則,考慮下面的模板函數(shù),模板函數(shù)接收一個右值引用作為模板參數(shù)。
| 12 | template<typename T>void foo(T&&); |
說白點,特殊模板參數(shù)推導規(guī)則其實就是引用折疊規(guī)則在模板參數(shù)為右值引用時模板情況下的應用,是引用折疊規(guī)則的一種情況。我們結合上文中的引用折疊規(guī)則,
解決完美轉發(fā)問題
我們已經(jīng)學習了模板參數(shù)為右值引用時的特殊模板參數(shù)推導規(guī)則,那么我們利用剛學習的知識來解決本文中待解決的完美轉發(fā)的例子。
| 123456789101112131415161718192021222324252627282930 | using namespace std;void fun(int &) { cout << "lvalue ref" << endl; }void fun(int &&) { cout << "rvalue ref" << endl; }void fun(const int &) { cout << "const lvalue ref" << endl; }void fun(const int &&) { cout << "const rvalue ref" << endl; }//template<typename T>//void PerfectForward(T t) { fun(t); }// 利用引用折疊規(guī)則代替了原有的不完美轉發(fā)機制template<typename T>void PerfectForward(T &&t) { fun(static_cast<T &&>(t)); }int main(){PerfectForward(10); // rvalue ref,折疊后t類型仍然為T && int a;PerfectForward(a); // lvalue ref,折疊后t類型為T &PerfectForward(std::move(a)); // rvalue ref,折疊后t類型為T && const int b = 8;PerfectForward(b); // const lvalue ref,折疊后t類型為const T &PerfectForward(std::move(b)); // const rvalue ref,折疊后t類型為const T && return 0;} |
例子中已經(jīng)對完美轉發(fā)的各種情況進行了說明,這里需要對PerfectForward模板函數(shù)中的static_cast進行說明。static_cast僅是對傳遞右值時起作用。我們看一下當參數(shù)為右值時的情況,這里的右值包括了const右值和非const右值。
| 1234567 | // 參數(shù)為右值,引用折疊規(guī)則引用前template<int && &&T>void PerfectForward(int && &&t) { fun(static_cast<int && &&>(t)); }// 引用折疊規(guī)則應用后template<int &&T>void PerfectForward(int &&t) { fun(static_cast<int &&>(t)); } |
可能讀者仍然沒有發(fā)現(xiàn)上述例子中的問題,“不用static_cast進行強制類型轉換不是也可以嗎?”。別忘記前文中仍然提到一個右值引用和右值之間關系的規(guī)則,如果右值引用有名字則為左值,如果右值引用沒有名字則為右值。。這里的變量t雖然為右值引用,但是是左值。如果我們想繼續(xù)向fun函數(shù)中傳遞右值,就需要使用static_cast進行強制類型轉換了。
其實在C++11中已經(jīng)為我們封裝了std::forward函數(shù)來替代我們上文中使用的static_cast類型轉換,該例子中使用std::forward函數(shù)的版本變?yōu)榱?#xff1a;
| 12 | template<typename T>void PerfectForward(T &&t) { fun(std::forward<T>(t)); } |
對于上文中std::move函數(shù)的實現(xiàn)也是使用了引用折疊規(guī)則,實現(xiàn)方式跟std::forward一致。
引用
總結
以上是生活随笔為你收集整理的C++11中的右值引用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 右值引用与转移语义
- 下一篇: 单例模式及C++实现代码