左值與右值
C++中左值與右值的概念是從C中繼承而來,一種簡單的定義是左值能夠出現再運算式的左邊或者右邊,而右值只能出現在運算式的右邊,
int a = 5; // a是左值,5是右值
int b = a; // b是左值,a也是左值
int c = a + b; // c是左值,a + b是右值
另一種區分左值和右值的方法是:有名字、能取地址的值是左值,沒有名字、不能取地址的值是右值,比如上述陳述句中a,b, c是變數可以取地址,所以是左值,而5和a + b無法進行取地址操作,因此是右值,C++中左值與右值的一個主要的區別是:左值可以被修改,而右值不可修改,
左值參考與右值參考
了解了左值與右值的概念后,接下來介紹下C++中的左值參考與右值參考,左值參考很簡單,就是一個變數的別名,系結到一個左值上:
int a = 1;
int& b = a; //a = 1,b = 1
b = 2; // a = 2,b = 2
這里b就等于a,在匯編層面其實和普通的指標一樣,對參考的修改(b)也會修改到被參考的物件(a),需要注意的是,因為參考實際是一個別名,因此必須初始化,即告訴編譯器是那個具體物件的別名,因此下列左值參考都是錯誤的:
int& a; // 錯誤!左值參考必須初始化
int& b = 10; // 錯誤!左值參考不能以臨時變數初始化(臨時變數沒有地址)
右值參考是C++11中新增的特性,顧名思義,右值參考就是用來系結到右值的參考,一個右值被系結到右值參考之后,原本需要被銷毀的此右值生命周期會延長至系結它的右值參考的生命周期,在匯編層面,右值參考和const參考所做的事情是一樣的,即產生臨時量來存盤常量,但是右值參考可以進行讀寫操作,而const參考只能進行讀操作,系結右值參考使用&&,具體使用如下:
int a = 5;
int& b = a; // 正確!b是一個左值參考
int&& c = 6; // 正確!c是一個右值參考,系結到右值6
int&& d = a * 2; // 正確!d是一個右值參考,系結到右值a * 2
int&& e = i; // 錯誤!不能將左值系結到右值參考
int& f = 7; // 錯誤!不能將右值系結到左值參考
const int& g = a * 3; // 正確!可以將右值系結到const 左值參考
可以看到我們雖然不能將右值系結到左值參考,但是可以將右值系結到const左值參考,
注意: 變數運算式都是左值!,變數可以看作是只有一個運算物件而沒有運算子的運算式,跟其他運算式一樣,變數運算式也有左值/右值屬性,變數運算式都是左值,因此我們不能將一個右值參考系結到一個右值參考型別的變數上,
int&& a = 5; // 正確!a是一個右值參考
int&& b = a; // 錯誤!a是一個左值,不能系結到右值參考
這里雖然a是右值參考型別,但是確實一個左值,因此無法系結到右值參考b上,因為在C++中,右值一般是臨時物件,但是系結到右值參考之后,其生命周期變長了,因此a是一個左值,我們不能將一個右值參考直接系結到一個變數上,即使是這個變數是右值參考型別也不行,具體的這個問題在后續的介紹forward的時候會詳細說明,
左值/右值參考的模板實參推斷
在另一篇文章中介紹了C++的模板型別推斷的幾種型別,可以總結為以下三種:
- ParamType 是一個指標或者參考(&),但是不是通用參考(&&)
- ParamType是一個通用參考(&&)
- ParamType既不是指標,也不是參考(&)或者通用參考(&&)
從左值參考函式引數推斷型別
當一個函式引數是模板的左值參考(T&)時,根據系結規則,只能傳遞一個左值實參,這個左值實參可以時const型別,也可以不是,如果實參時const的,那么T就會被推導為const型別
template<typename T>
void func(T& param);
int a = 0;
const int b = a;
func(a); // T被推導為int,param型別為int&
func(b); // T被推導為const int,param型別為const int&
func(5); // 錯誤!實參必須是一個左值!
如果一個函式的型別時const T&,那么根據系結規則,可以傳遞任何型別的實參:const或者非const,左值或者右值,由于函式型別本身已經是const,因此T的推導結果不會是一個const,因為const已經是函式引數型別的一部分了,
template<typename T>
void func(const T& param);
int a = 0;
const int b = a;
func(a); // T被推導為int,param型別為const int&
func(b); // T被推導為int,param型別為const int&
func(5); // 正確!const T&可以系結一個右值,T為int
可以看到,當函式引數型別為const T&時,可以接受一個右值實參,而函式引數型別為 T& 時是不可以的,
從右值參考函式引數推斷型別
當一個函式的引數是一個右值參考(T&&)時,根據系結規則可以傳遞一個右值實參,類似左值參考推導,右值參考推導得到的T的型別為右值的型別:
template<typename T>
void func(T&& param);
func(5); // 實參5為右值,T被推導為int型別
與不能給右值參考賦值左值不同,右值參考函式的模板實參卻可以接受一個左值的輸入,當我們將一個左值傳遞給函式的右值參考引數時,且此右值參考指向模板引數型別(T&&)時,編譯器推導模板型別引數為實參的左值參考型別:
template<typename T>
void func(T&& param);
int a = 1;
func(a); // T被推導為int&,而不是int
如上述推導所示,當傳入一個左值a時,T被推導為int&,而不是int,對應的param的型別為int& &&,根據參考折疊的規則,int& &&被折疊為int&,
參考折疊規則
T& & ,T& && 和T&& &都會被折疊為T&
T&& &&被折疊為T&&
參考折疊的規則告訴我們:如果一個函式的引數時指向模板引數型別的右值參考(如T&&),則可以傳遞給它任意型別的實參,如果傳遞的左值實參,那么T將會推導成為一個左值參考,函式引數被實體化為一個普通的左值參考(T&),這種參考叫做“通用參考”,
右值參考與通用參考
C++中T&&有兩種不同的意思,第一種是右值參考,用于系結到右值上,它們主要存在的原因是為了宣告某個物件可以被移動,T&&的第二層意思是,它既可以是一個右值參考,也可以是一個左值參考,這種參考在代碼里看起來像是右值參考(T&&),又可以表現的像是左值參考(T&),它既可以系結到右值,也可以系結到左值,還可以系結到const和no_const物件上,幾乎可以系結到任何東西,這種參考叫做“通用參考”,在兩種情況下會出現通用參考,最常見的就是函式模板引數:
template<typename T>
void func(T&& param); // param是一個通用參考
第二種情況是auto宣告符:
auto&& a = b; //a是一個通用參考
以上兩種情況的共同之處在于都是型別推導,在func內部,param型別需要被推導,在auto宣告中,a的型別也需要被推導,而如果帶有&&而不需要推導,則就是普通的右值參考:
void func(A&& param); // 沒有型別推導,param是一個右值參考
A&& a = b; // 沒有型別推導,a是一個右值參考
由于參考必須初始化,通用參考也一樣,一個通用參考的初始值決定了其具體代表的是一個左值參考還是右值參考,如果初始值是一個左值,那么通用參考對應的就是左值參考,如果初始值是一個右值,那么通用參考對應的就是一個右值參考,
template<typename T>
void func(T&& param); // param是一個通用參考
int a = 1;
func(a); // a是左值,T被推導為int&,引數param的型別是int&,是一個左值參考
func(5); // 5是右值,T被推導為int,引數param的型別是int&&,是一個右值參考
需要注意的是,判斷一個參考是不是通用參考,型別推導是必要的,但是并不是型別推導就是通用參考,還需要看是不是準確的T&&,如:
template<typename T>
void func(std::vector<T>&& param); // param是一個右值參考
template<typename T>
void func(const T&& param); // param是一個右值參考
上述模板函式func被呼叫的時候,型別T也會被推導,但是引數param的型別并不是T&&,而是一個std::vector
理解std::move()
有了上述的知識基礎之后,C++中的move函式功能就很好理解了,std::move的主要作用是將一個左值/右值無條件的轉換為右值,但是函式本身并不移動任何東西,只是進行型別的轉換,那么這種轉換是如何做到的呢?我們來看下std::move具體實作的代碼:
template<class T>
typename remove_reference<T>::type&& move(T&& param)
{
using returnType = typename remove_reference<T>::type&&;
return static_cast<returnType>(param);
}
通過原始碼可以看到,std::move接受一個通用參考的引數,函式回傳一個&&表明std::move函式回傳的是一個右值參考,這里remove_reference
template<typename T>
decltype(auto) move(T&& param)
{
using returnType = remove_reference_t<T>&&;
return static_cast<returnType>(param);
}
讓我們通過以下的代碼示例具體分析下std::move是如何作業的:
string s1("hello"),s2;
s2 = std::move(string("world")); // 從右值移動資料
s2 = std::move(s1); // 將左值轉換為右值
在第一個賦值中,傳遞給move的實參是一個右值,當向一個右值參考傳遞一個右值時,推導的型別即被參考的型別,因此在std::move(string("world"))中:
- T被推導為string
- returnType為string
- move的回傳型別為string&&
- move的函式引數param的型別為string&&
則函式std::move被推導為:
string&& move(string&& param)
{
return static_cast<string&&>(param);
}
由于param已經時右值參考型別,因此實際上move函式什么也沒做,
在第二個賦值中,傳給std::move的引數是一個左值,則在std::move(s1)中:
- T被推導為string&
- returnType為string
- move的回傳型別為string&&
- move的函式引數param的型別為string&
則函式std::move被推導為:
string&& move(string& param)
{
return static_cast<string&&>(param);
}
可以看到引數param被static_cast轉換為sting&&,在C++中,從一個左值static_cast到一個右值參考時允許的,
從以上的示例可以看到,不管傳入的是左值還是右值,最終move都會回傳一個右值,
理解std::forward()
std::forward與std::move實作的功能是類似的,只不過std::move總是無條件的將它的引數轉換為右值,而std::forward只有在滿足一定的條件下才會執行轉換,std::forward最常見的使用場景是一個模板函式,接受一個通用參考引數,并將其傳遞給另外的函式:
void Process(const A& lvalue); // 處理左值
void Process(A&& rvalue); // 處理右值
template<typename T>
void PrintAndProcess(T&& param)
{
Print("Some Log");
process(std::forward<T>(param))
}
現在考慮兩次對PrintAndProcess的呼叫,一次引數為左值,一次引數為右值
A a;
PrintAndProcess(a); // 左值引數
PrintAndProcess(std::move(a)); // 右值引數
在PrintAndProcess函式內部,引數param被傳遞給process函式,process函式分別對左值和右值進行了多載,傳入PrintAndProcess左值引數時希望process左值版本被呼叫,傳入PrintAndProcess右值引數時,process右值版本被呼叫,但是前面我們提過,一個右值參考的變數,其本身時一個左值,因此無論傳給PrintAndProcess函式的實參時左值還是右值,最終呼叫process函式都是左值版本,為了解決這個問題,我們就需要一種機制:當傳入PrintAndProcess函式的實參是右值時,呼叫的時process的右值版本,這就是std::forward的使用場景:只把由右值初始化的引數,轉換為右值,
那么std::forward如何知道param引數是被一個左值還是一個右值給初始化的呢?我們來看下std::forward實作的原始碼:
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept{
// forward an lvalue as either an lvalue or an rvalue
return (static_cast<T&&>(arg));
}
template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
// forward an rvalue as an rvalue
return (static_cast<T&&>(arg));
}
對于左值的轉發,首先通過獲取型別type,定義args為左值參考的左值變數,然后通過static_cast<T&&>進行強制轉換,這里T&&會發生參考折疊,當T被推導為左值參考時,則為T&& &,折疊為T&,當推導為右值參考時,則本身為T&&,forward回傳值與static_cast都為T&&,
對于右值的轉發不同于左值,只有當型別時右值時才進行static_cast轉換,arg為右值參考的左值變數,通過cast轉換為T&&,
對應到上述PrintAndProcess函式中我們進行分析:
- 當PrintAndProcess(a),傳入的為左值A時,T被推導為A&,std::forward回傳值和static_cast被推導為A& &&,折疊為A&,回傳一個左值,
- 當PrintAndProcess(std::move(a)),傳入為右值時,T被推導為A,在std::forward回傳值和static_cast被推導為T&&,回傳一個右值,
std::move 和 std::forward對比
- std::move執行到右值的無條件轉換,就其本身而言,它沒有move任何東西,
- std::forward只有在它的引數系結到一個右值上的時候,它才轉換它的引數到一個右值,
- std::move和std::forward只不過就是執行型別轉換的兩個函式;std::move沒有move任何東西,std::forward沒有轉發任何東西,在運行期,它們沒有做任何事情,它們沒有產生需要執行的代碼,一個byte都沒有,
- std::forward
()不僅可以保持左值或者右值不變,同時還可以保持const、Lreference、Rreference、validate等屬性不變;
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549552.html
標籤:其他
上一篇:陣列的演算法
