【轉載】右值引用
【本文轉自】:
作者: 蘇丙榲
連結: https://subingwen.cn/cpp/rvalue-reference/
來源: 愛程式設計的大丙
1. 右值引用
1.1 右值
C++11 增加了一個新的型別,稱為右值引用( R-value reference),標記為 &&。在介紹右值引用型別之前先要了解什麼是左值和右值:
左值(l-value - locator value)
是指儲存在記憶體中、有明確儲存地址(可取地址)的資料;右值(r -value - read value)
是指可以提供資料值的資料(不可取地址);
通過描述可以看出,區分左值與右值的便捷方法是:可以對其取地址(&)就是左值,否則為右值 。所有有名字的變數或物件都是左值,而右值是匿名的。
int a = 520;
int b = 1314;
a = b;
一般情況下,位於 = 前的為左值,位於 = 後邊的為右值。也就是說例子中的 a, b 為左值,520、1314為右值。a=b 是一種特殊情況,在這個例子中 a, b 都是左值,因為變數 b 是可以被取地址的,不能視為右值。
C++11 中右值可以分為兩種:一個是將亡值( xvalue, expiring value),另一個則是純右值( prvalue, PureRvalue):
- 純右值:非引用返回的臨時變數、運算產生的臨時變數、原始字面量和 lambda 等
- 將亡值:與右值引用相關的,比如,T&& 型別函式的返回值、 std::move 的返回值等。
1.2 右值引用
右值引用就是對一個右值進行引用的型別。因為右值是匿名的,所以我們只能通過引用的方式找到它。無論宣告左值引用還是右值引用都必須立即進行初始化,因為引用型別本身並不擁有所繫結物件的記憶體,只是該物件的一個別名。通過右值引用的宣告,該右值又“重獲新生”,其生命週期與右值引用型別變數的生命週期一樣,只要該變數還活著,該右值臨時量將會一直存活下去。
關於右值引用的使用,參考程式碼如下:
#include <iostream> int&& value = 520; class Test { public: Test() { std::cout << "construct: my name is jerry" << std::endl; } Test(const Test& a) { std::cout << "copy construct: my name is tom" << std::endl; } }; Test getObj() { return Test(); } int main() { int a1; int&& a2 = a1; // ERROR: 'initializing': cannot convert from 'int' to 'int &&', message : You cannot bind an lvalue to an rvalue reference Test& t = getObj(); // ERROR: 'initializing': cannot convert from 'Test' to 'Test &', message : A non-const reference may only be bound to an lvalue Test&& t1 = getObj(); const Test& t2 = getObj(); return 0; }
- 在上面的例子中 int&& value = 520; 裡面 520 是純右值,value 是對字面量 520 這個右值的引用。
- 在 int &&a2 = a1; 中 a1 雖然寫在了 = 右邊,但是它仍然是一個左值,使用左值初始化一個右值引用型別是不合法的。
- 在 Test& t = getObj() 這句程式碼中語法是錯誤的,右值不能給普通的左值引用賦值。
- 在 Test && t = getObj(); 中 getObj() 返回的臨時物件被稱之為將亡值,t 是這個將亡值的右值引用。
- const Test& t = getObj() 這句程式碼的語法是正確的,常量左值引用是一個萬能引用型別,它可以接受左值、右值、常量左值和常量右值。
2. 效能優化
在 C++ 中在進行物件賦值操作的時候,很多情況下會發生物件之間的深拷貝,如果堆記憶體很大,這個拷貝的代價也就非常大,在某些情況下,如果想要避免物件的深拷貝,就可以使用右值引用進行效能的優化。例如:
#include <iostream>
class Test
{
public:
Test() : m_num(new int(100))
{
std::cout << "construct: my name is jerry" << std::endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
std::cout << "copy construct: my name is tom" << std::endl;
}
~Test()
{
delete m_num;
std::cout << "delete m_num" << std::endl;
}
int* m_num{ nullptr };
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
std::cout << "t.m_num: " << *t.m_num << std::endl;
return 0;
};
程式執行結果如下:
construct: my name is jerry
copy construct: my name is tom
delete m_num
t.m_num: 100
delete m_num
通過輸出的結果可以看到呼叫 Test t = getObj(); 的時候呼叫拷貝建構函式對返回的臨時物件進行了深拷貝得到了物件 t,在 getObj() 函式中建立的物件雖然進行了記憶體的申請操作,但是沒有使用就釋放掉了。如果能夠使用臨時物件已經申請的資源,既能節省資源,還能節省資源申請和釋放的時間,如果要執行這樣的操作就需要使用右值引用了,右值引用具有移動語義,移動語義可以將資源(堆、系統物件等)通過淺拷貝從一個物件轉移到另一個物件這樣就能減少不必要的臨時物件的建立、拷貝以及銷燬,可以大幅提高 C++ 應用程式的效能。
使用移動建構函式例子:
#include <iostream>
class Test
{
public:
Test() : m_num(new int(100))
{
std::cout << "construct: my name is jerry" << std::endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
std::cout << "copy construct: my name is tom" << std::endl;
}
// 新增移動建構函式
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;
cout << "move construct: my name is sunny" << endl;
}
~Test()
{
if (m_num != nullptr)
{
delete m_num;
m_num = nullptr;
std::cout << "delete m_num" << std::endl;
}
}
int* m_num{ nullptr };
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
std::cout << "t.m_num: " << *t.m_num << std::endl;
return 0;
};
程式執行結果如下:
construct: my name is jerry
move construct: my name is sunny
t.m_num: 100
delete m_num
通過修改,在上面的程式碼給 Test 類添加了移動建構函式
(引數為右值引用型別),這樣在進行 Test t = getObj()
; 操作的時候並沒有呼叫拷貝建構函式進行深拷貝,而是呼叫了移動建構函式,在這個函式中只是進行了淺拷貝,沒有對臨時物件進行深拷貝,提高了效能。
如果不使用移動構造或拷貝構造,在執行 Test t = getObj() 的時候也是進行了淺拷貝,但是當臨時物件被析構的時候,類成員指標 int* m_num; 指向的記憶體也就被析構了,物件 t 也就無法訪問這塊記憶體地址了。物件 t 再去釋放該記憶體時,程式就會崩潰,雙重釋放。
在測試程式中 getObj() 的返回值就是一個將亡值,也就是說是一個右值,在進行賦值操作的時候如果 = 右邊是一個右值,那麼移動建構函式就會被呼叫
。移動構造中使用了右值引用,會將臨時物件中的堆記憶體地址的所有權轉移給物件t,這塊記憶體被成功續命,因此在t物件中還可以繼續使用這塊記憶體。
對於需要動態申請大量資源的類,應該設計移動建構函式,以提高程式效率。需要注意的是,我們一般在提供移動建構函式的同時,也會提供常量左值引用的拷貝建構函式,以保證移動不成還可以使用拷貝建構函式。
3. && 的特性
在 C++ 中,並不是所有情況下 && 都代表是一個右值引用,具體的場景體現在模板和自動型別推導中,如果是模板引數需要指定為 T&&,如果是自動型別推導需要指定為 auto &&,在這兩種場景下 && 被稱作未定的引用型別。另外還有一點需要額外注意 const T&& 表示一個右值引用,不是未定引用型別。
先來看第一個例子,在函式模板中使用 &&:
template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10);
int x = 10;
f(x);
f1(10);
在上面的例子中函式模板進行了自動型別推導,需要通過傳入的實參來確定引數 param 的實際型別。
- 第 4 行中,對於 f(10) 來說傳入的實參 10 是右值,因此 T&& 表示右值引用
- 第 6 行中,對於 f(x) 來說傳入的實參是 x 是左值,因此 T&& 表示左值引用
- 第 7 行中,f1(10) 的引數是 const T&& 不是未定引用型別,不需要推導,本身就表示一個右值引用
int main()
{
int x = 520, y = 1314;
auto&& v1 = x;
auto&& v2 = 250;
decltype(x)&& v3 = y; // error
std::cout << "v1: " << v1 << ", v2: " << v2 << std::endl;
return 0;
};
- 第 4 行中 auto&& 表示一個整形的左值引用
- 第 5 行中 auto&& 表示一個整形的右值引用
- 第 6 行中 decltype(x)&& 等價於 int&& 是一個右值引用不是未定引用型別,y 是一個左值,不能使用左值初始化一個右值引用型別。