【轉載】c++右值引用以及使用
轉自:https://www.cnblogs.com/likaiming/p/9045642.html
前幾天看了一篇文章《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) 0x080483fa lea -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完美保持了引數的型別。