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