左值與右值
這在之前的一篇博文中有詳細介紹,這裏再簡單說一下。
-
左值:
可使用&符號取地址
可位於賦值操作符=的左側,也可位於右側 -
右值:
不能使用&符號取地址
只能位於賦值操作符的右側
老式的swap函數
template<class T> swap(T& a, T& b) // 老式的swap函數
{
T tmp(a);
a = b;
b = tmp;
}
如果T是一個拷貝代價相當高昂的類型,例如string和vector,那麼上述swap()操作將很耗資源。
標準庫已經針對string和vector的swap()進行了特化來解決這個問題。
新式的swap函數
template <class T>
void swap(T& a, T& b) // “完美swap”(大多數情況下)
{
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
在C++11的標準庫中,所有的容器都提供了移動構造函數和移動賦值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。
右值引用
右值引用,解決2個問題:
- 移動語義 (這個比較簡單,本文就不介紹了)
- 完美轉發 (右值引用解決了完美轉發前一半的問題,後一半是std::forward解決的)
右值引用有3種
-
不具名右值引用
static_cast<T&&>(t)
std::move() -
具名右值引用
可以用&符號取地址,所以具名右值引用是左值。
其實,具名變量都是左值。
具名右值引用,讓本應銷燬的純右值又“重獲新生”,使得其生命週期和右值引用類型變量的生命週期一樣長。
在C++11之前,C++98/03的時候,使用 const 左值引用來使得右值的生命週期延長以進行性能優化,如 const A& a = GetA(); -
萬能型引用(universal reference)
包含2種實例:- auto&&類型遍歷
- 函數模板的某個參數被聲明爲T&&類型,並且該形參到底是左值還是右值需經過推導才能確定
這裏的重點是,僅僅是當發生自動類型推導(如函數模板的類型自動推導,或auto關鍵字)的時候,T&&纔是universal references.
template<typename T>
void f(T&& param); // param是萬能型引用,因爲T&&不是確定的類型
template<typename T>
class Test {
Test(Test&& rhs); // rhs是普通的右值引用,因爲Test&&已經是確定的類型
};
萬能型引用類型的變量的實際類型由它所綁定的值來確定。如果綁定左值,那該變量就是左值;如果綁定右值,那該變量就是右值。(注:這應該就是C++標準的規定,編譯器廠商實現之。)
對於 auto&& 類型來說,如果所賦之值爲左值,則 auto&& 變量爲左值;如果所賦之值爲右值,則 auto&& 變量爲右值。
class X {};
// var1是具名右值引用,具名右值引用是左值,所以var1是左值; var1綁定到右值X()
X&& var1 = X();
// var2是萬能型引用,綁定到左值var1;因此,var2是左值,類型推導爲 X&
auto&& var2 = var1;
// var3是萬能型引用,綁定到右值X(); 因此,var3是右值,類型推導爲 X&&
auto&& var3 = X();
對於函數模板的 T&& 的萬能型引用形參,與上述也是類似。
class X{};
template<typename T>
void g(std::vector<typename T>&& p1); // p1是右值引用
template<typename T>
void f(T&& p2); // p2是萬能型引用
X a; // a是左值
f(a); // p2綁定左值a,因此 p2 爲左值,其類型推導爲 X& 類型
f(X()); // p2綁定右值, 因此 p2 爲右值,其類型推導爲 X&& 類型
另外,注意 const auto && 和 函數模板參數的 const T&& 類型參數 都是普通右值引用,而不是萬能型引用。
關於萬能型引用,Scott的這篇文章很不錯。
具名右值引用是左值的一個場景
假設有一個類Base,並且通過重載拷貝構造函數和賦值操作符實現了move語義:
Base(Base const & rhs); // non-move semantics
Base(Base&& rhs); // move semantics
然後有一個繼承自Base的類Derived. 爲了保證Derived對象中的Base部分能夠正確實現move語義,必須也重載Derived類的拷貝構造函數和賦值操作符。
先看左值版本的拷貝構造函數:
Derived(Derived const & rhs)
: Base(rhs)
{
// Derived-specific stuff
}
再看錯誤的右值版本的拷貝構造函數:
Derived(Derived&& rhs)
: Base(rhs) // 錯誤:rhs是個左值
{
// Derived-specific stuff
}
而正確的右值版本的拷貝構造函數是這樣的:
Derived(Derived&& rhs)
: Base(std::move(rhs)) // Correct, call Base(Base&& rhs)
{
// Derived-specific stuff
}
move語義與編譯器優化
有一個下面這樣的函數
X foo()
{
X x;
// Do something to x
// ...
return x;
}
既然返回的是一個臨時變量,爲了避免“將x拷貝給臨時變量”這個過程,能否將最後一句 return x; 改爲 return std::move(x); 呢?
不能這麼做。因爲現在的編譯器基本上都會做返回值優化(Return Value Optimization),也就是說,對於 return x; 這種操作,編譯器不會去做copy/move構造函數,而是相當於在函數返回的地方直接創建對象,這樣更加優化。反之,如果加了 std::move(x),就達不到C++標準中所說的 Copy Elision,從而仍然存在copy的動作。
關於這個問題,結論與基本解釋就是如上所述,更多細節可參見下面2篇文章:
- https://stackoverflow.com/questions/19267408/why-does-stdmove-prevent-rvo
- https://stackoverflow.com/questions/14856344/when-should-stdmove-be-used-on-a-function-return-value
完美轉發
-
目的:
其他函數可以因轉發來的參數的左右值屬性不同而進行不同處理: 參數爲左值時實施拷貝語義;參數爲右值時實施移動語義。 -
要解決的問題:
函數模板在向其他函數轉發(即傳遞)該函數模板自身參數(形參)時該如何保留該參數(實參)的左右值屬性。
即,如果實參是左值,那麼它就應該被轉發爲左值;如果實參是右值,它就應該被轉發爲右值。 -
實現的難點:
即便函數模板的參數是萬能型引用,到了函數模板內部,該萬能型引用因爲同時又是具名的,所以仍爲左值;從而使得傳遞給其他函數的時候傳遞的仍是左值。這就無法轉發右值了。
見下面的程序:
#include<iostream>
using namespace std;
struct X {};
void inner(const X&) {cout << "inner(const X&)" << endl;}
void inner(X&&) {cout << "inner(X&&)" << endl;}
template<typename T>
void outer(T&& t) {inner(t);}
int main()
{
X a;
outer(a); // inner(const X&)
outer(X()); // inner(const X&)
}
所以,右值引用(其實是萬能型引用),只把完美轉發問題解決了一半,即只能保證形參 T&& 的實際類型可以根據實參來決定是左值引用還是右值引用;另一半問題則是藉助了 std::forward(t) 解決的。
std::forward<T>(t)
標準庫函數 std::forward<T>(t) 有兩個參數:模板參數 T 與 函數參數 t:
- 當T爲左值引用時,t將被轉換爲無名左值引用
- 當T爲非引用類型或右值引用時,t將被轉換爲無名右值引用
這裏的重點是,若t是具名右值引用, std::forward<T>(t) 將也是一個右值引用,而非左值。 從而解決了上述的另一半問題。
參考代碼如下:
#include<iostream>
using namespace std;
struct X {};
void inner(const X&) {cout << "inner(const X&)" << endl;}
void inner(X&&) {cout << "inner(X&&)" << endl;}
template<typename T>
void outer(T&& t) {inner(forward<T>(t));}
int main()
{
X a;
outer(a); // inner(const X&)
outer(X()); // inner(X&&)
}
綜上所述,右值引用主要是用來解決移動語義和完美轉發這2個問題。
關於完美轉發與引用摺疊,後續應該還會有文章談到。本文終。
(完)