c++右值引用以及使用
前幾天看了一篇文章《4行代碼看看右值引用》 覺得寫得不錯,但是覺得右值引用的內容還有很多可以去挖掘學習,所以總結了一下,希望能對右值引用有一個更加深層次的認識
一、幾個基本概念
1.1左值和右值
左值和右值的區分標準在於能否獲取地址。
最早的c++中,左值的定義表示的是可以獲取地址的表達式,它能出現在賦值語句的左邊,對該表達式進行賦值。但是修飾符const的出現使得可以聲明如下的標識符,它可以取得地址,但是沒辦法對其進行賦值:
const int& i = 10;
右值表示無法獲取地址的對象,有常量值、函數返回值、Lambda表達式等。無法獲取地址,但不表示其不可改變,當定義了右值的右值引用時就可以更改右值。
1.2 左值引用和右值引用
傳統的c++引用被稱為左值引用,用法如下:
int i = 10; int & ii = I;
C++ Primer Plus 第6版18.1.9中說到,c++11中增加了右值引用,右值引用關聯到右值時,右值被存儲到特定位置,右值引用指向該特定位置,也就是說,右值雖然無法獲取地址,但是右值引用是可以獲取地址的,該地址表示臨時對象的存儲位置。語法如下:
int && iii = 10;
1.3 左值引用和右值引用的匯編代碼
以下匯編都是x86匯編
寫一段簡單的語句,看其匯編
int i = 1; int & ii = i;
0x080483f3 movl $0x1,-0x10(%ebp) 0x080483falea -0x10(%ebp),%eax 0x080483fd mov %eax,-0x8(%ebp)
第一句是將1賦值給i,第二句將i的地址放入eax中,第三句將eax中的值傳給ii。可見引用就是從一個變量處取得變量的地址,然後賦值給引用變量。
再看一句右值引用的匯編
int && iii = 10;
0x08048400 mov $0xa,%eax 0x08048405 mov %eax,-0xc(%ebp) 0x08048408 lea -0xc(%ebp),%eax 0x0804840b mov %eax,-0x4(%ebp)
第一句將10賦值給eax,第二句將eax放入-0xc(%ebp)處,前面說到“臨時變量會引用關聯到右值時,右值被存儲到特定位置”,在這段程序中,-0xc(%ebp)便是該臨時變量的地址,後兩句通過eax將該地址存到iii處。
通過上述代碼,我們還可以發現,在上述的程序中-0x4(%ebp)存放著右值引用iii,-0x8(%ebp)存放著左值引用,-0xc(%ebp)存放著10,而-0x10(%ebp)存放著1,左值引用和右值引用同int一樣是四個字節(因為都是地址)
同時,我們可以深入理解下臨時變量,在本程序中,有名字的1(名字為i)和沒有名字的10(臨時變量)的值實際是按同一方式處理的,也就是說,臨時變量根本上來說就是一個沒有名字的變量而已。它的生命周期和函數棧幀是一致的。也可以說臨時變量和它的引用具有相同的生命周期。
1.4 const左值引用
如果寫如下代碼,定義一個左值引用,將其值置為一個常量值,則會報錯:
int & i = 10;
原因很明顯,左邊是一個左值引用,而右邊是一個右值,無法將左值引用綁定到一個右值上。
但是如果是一個const的左值引用,是可以綁定到右值上的。即如下寫法是符合語法規範的:
const int & i = 10;
這段程序的匯編代碼如下:
0x08048583 mov $0xa,%eax 0x08048588 mov %eax,-0x8(%ebp) 0x0804858b lea -0x8(%ebp),%eax 0x0804858e mov %eax,-0x4(%ebp)
易知-0x4(%ebp)處存放著i,-0x8(%ebp)處則存放著臨時對象10,程序將10的地址存放到了i處。看到這裏會發現const引用在綁定右值時和右值引用並沒有什麽區別。
1.5 左值引用和右值引用的相互賦值
能將右值引用賦值給左值引用,該左值引用綁定到右值引用指向的對象,在早期的c++中,引用沒有左右之分,引入了右值引用之後才被稱為左值引用,所以說左值引用其實可以綁定任何對象。這樣也就能理解為什麽const左值引用能賦予常量值。
int&& iii = 10; int& ii = iii; //ii等於10,對ii的改變同樣會作用到iii
二、右值引用和移動語義
在舊的c++中,出現了很多的不必須要的拷貝,因為在某些情況下,對象拷貝完之後就下來就銷毀了。新標準引入了移動操作,減少了很多的復制操作,而右值引用正式為了支持移動操作而引入的新的引用類型。
2.1 標準庫move函數
根據右值引用的語法規則可知,不能將右值引用綁定到一個左值上,c++11引入右值引用,並且提供了move函數,用來獲得綁定到左值上的右值引用,此函數定義在頭文件utility中。
Int &&iii = move(ii)
調用move之後,必須保證除了對ii復制或銷毀它外,我們將不再使用它,在調用move之後,我們不能對移動源後對象做任何假設。
2.2 模板實參推斷和引用
為了理解move函數的實現,首先需要理解模板實參推斷和引用。
當左值引用作為參數時,看幾個例子:
template<class T> void f1(T&) {} f1(i) //i是一個int,模板參數類型T是int f1(ci) //ci是一個const int,模板參數T是const int fl(5) //錯誤:傳遞給一個&參數的實參必須是一個左值
如果函數的參數是const的引用時:
template<class T> void f2(const T&) {} f2(i) //i是一個int,模板參數類型T是int,因為非const可以轉化為const f2(ci) //ci是一個const int,模板參數T是int f2(5) //看前面,const的引用可以綁定右值,T是int
當參數是右值引用時,
template<class T> void f3(T&&) {} f3(5) // T是int
2.3 引用折疊和右值引用參數
按照道理來說,f3(i)是應該不正確的,因為無法將右值引用綁定到一個左值上,但是,c++中有兩個正常綁定規則的例外,允許這種綁定。這兩個例外規則是move正確工作的基礎
例外1:右值引用的類型推斷。當我們將一個左值傳遞給函數的右值引用作為參數時(函數參數為T&&),編譯器推斷模板類型參數為實參的左值引用類型,,因此,調用f3(i)時,T被推斷為int&,而不是int。並且,模板函數中對參數的改變會反映到調用時傳入的實參。
通常,我們不能直接定義一個引用的引用,但是同過類型別名(使用typedef)和模板間接定義是可以的。
例外2:引用折疊。當定義了引用的引用時,則這些引用形成了“折疊”,所有的情況下(除了一個例外),引用會折疊成一個普通的左值引用類型。這個例外就是右值引用的右值引用:
l X& &&、X& &&、X&& &都折疊成X&
l 類型X&& &&折疊成X&&
2.4 理解右值引用折疊和右值引用類型推斷
對於函數f3而言,根據右值引用類型推斷規則可以知道如下結果:
f3(i) //實參是左值,模板參數T是int& f3(ci) //實參是左值,模板參數T是一個const int&
但是當T被推斷為int&時,函數f3會實例化成如下的樣子:
void f3<int&>(int& &&)
然後根據右值引用折疊規則可以知道,上述實例化方式應該被折疊成如下樣子:
void f3<int&>(int&)
這兩個規則導致了兩個重要的結果:
l 如果一個函數參數是一個指向模板類型參數的右值引用,如T&&,則它能被綁定到一個左值,且
l 如果實參是一個左值,則推斷出的模板實參類型將時一個左值引用,且函數參數被實例化為一個普通左值引用參數(T&)
值得註意,參數為T&&類型的函數可以接受所有類型的參數,左值右值均可。在前面,同樣介紹過,const的左值引用做參數的函數同樣也可以接受所有類型的參數。
2.5 當右值引用作為函數模板參數時
通過前面,我們了解到當右值引用作為函數模板參數時,類型T會被推斷為一個引用類型。這一特性會影響模板函數內部的代碼,看下面一段代碼:
template<class T> void f3(T&& val) { T t = val; t = fcn(t); if(val == t){…} }
假如以左值i來調用該函數,那麽T被推斷為int&,將t綁定到val之上,對t的更改就被應用到val,則if判斷條件永遠為true。
右值引用通常用於兩種情況,模板轉發其實參、模板被重載。下面都會介紹到
前面說到,const左值引用做參數和右值引用做參數一樣,是可以匹配所有的參數類型,但當重載函數同時出現時,右值引用做參數的函數綁定非const右值,const左值引用做參數的函數綁定左值和const右值(非const右值就是通過右值引用來引用的右值,雖然無法獲取右值的地址,但是可以通過定義右值引用來更改右值):
Template<class T> void f(T&&) //綁定到非const右值 Template<class T> void f(const T&) //左值和const右值
2.6 move函數實現
vs2017中move函數的定義如下
using remove_reference_t = typename remove_reference<_Ty>::type; template<class _Ty> constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT { return (static_cast<remove_reference_t<_Ty>&&>(_Arg)); } template<class _Ty> struct remove_reference { // remove reference using type = _Ty; }; template<class _Ty> struct remove_reference<_Ty&> { // remove reference using type = _Ty; }; template<class _Ty> struct remove_reference<_Ty&&> { // remove rvalue reference using type = _Ty; };
使用右值引用作為參數,前面說過,可以匹配所有類型。以下兩種方式都是正確的:
string s1(“s1”),s2 s2 = move(string(“bye!”)) //_Ty推斷為string s2 = move(s1) //_Ty推斷為string&
至於remove_reference就好理解了
綜上,可以發現move函數不管傳入什麽類型參數,不管是左值還是右值,都會返回其右值引用類型
三、轉發
某些函數需要將其中一個或多個實參連同類型不變地轉發給其他函數,在這種情況下,我們需要保持被轉發實參的所有性質,包括實參是否是const的、以及是左值還是右值。
有如下的兩個函數,在flip中調用f:
void f(int v1, int &v2) { cout << v1 << " " << ++v2 << endl; } template <typename F, typename T1, typename T2> void flip(F f, T1 t1, T2 t2) { f(t2, t1); }
我們會發現f會改變第二個參數的值,但是通過flip調用f之後就不會改變
f(42, i) flip(f, j, 42)
模板被實例化成如下:
void flip (void(*fcn)(int, int&), int t1, int t2);
j的值被拷貝到t1中,所以flip中的f只會改變t1,而不會改變j
3.1 定義能保持類型信息的函數參數
如果將flip的參數定義成右值引用,根據上面描述過的規則,當給flip傳入引用時,T1被推斷為int&,t1則被折疊成int&,完美保持了實參的類型。
template <typename F, typename T1, typename T2> void flip(F f, T1&& t1, T2&& t2) { f(t2, t1); }
但是當函數f接受右值引用作為參數的時候,flip就不能正常工作了
void f(int &&I, int j) { cout << i << “ ” << j << endl; } flip(g,i,42) //錯誤,不能從一個左值實例化int&&
註意,這裏的f和g都不是模板函數,所以說前面提到的右值引用作為參數時的兩個例外不能成立。所以這裏是錯誤的
3.2 使用forward保持類型信息
Forward需要顯示提供實參類型,返回該實參類型的右值引用(在前面可以看到,右值引用是可以賦值給左值引用的)
void flip(F f, T1&& t1, T2&& t2) { f(forward<T2>(t2), forward<T1>(t1)); }
當顯式實參T是int&&時,forward返回int&& &&,折疊成int&&。當當顯式實參T是int&時,forward返回int& &&,折疊成int&。所以說forward完美保持了參數的類型。
c++右值引用以及使用