左值(Lvalue)與右值(Rvalue)
英文含義:
-
左值(Lvalue):Locator value,意味著它指向一個具體的記憶體位置。
-
右值(Rvalue):Read value,指的是可以讀取的資料,但不一定指向一個固定的記憶體位置。
定義
- 左值:指的是一個持久的記憶體地址。左值可以出現在賦值操作的左側或右側。例如,變數、陣列的元素、對物件成員的引用等都是左值。
- 右值:通常是臨時的、不能有多個引用的值,它們不指向持久的記憶體地址。右值可以出現在賦值操作的右側,但不能出現在左側。字面量(如42、3.14)、臨時物件、以及返回臨時物件的表示式等都是右值。
完美轉發(Perfect Forwarding)
完美轉發是C++11引入的一個概念,其目的是允許函式模板將引數以原來的左值或右值的形式轉發到其他函式。這是透過引用摺疊規則和std::forward
函式實現的。完美轉發的一個關鍵應用場景是模板函式中,我們希望將接收到的引數以完全相同的形式(保持其左值或右值性質)傳遞給另一個函式時使用。
引用摺疊規則
在模板函式或類中,當一個引用的引用被形成時,它們會摺疊成單一的引用
T& &
,T& &&
,T&& &
都會被摺疊為T&
T&& &&
會被摺疊為T&&
示例
- 當
wrapper(lv)
被呼叫時,lv
是一個左值,因此模板引數T
被推斷為int&
(左值引用)。由於引用摺疊規則,T&&
摺疊為int&
。因此,std::forward<T>(arg)
將arg
作為左值引用轉發給process
函式,呼叫process(int& i)
版本。 - 當
wrapper(20)
被呼叫時,20
是一個右值,因此模板引數T
被推斷為int
。由於T
是一個非引用型別,T&&
就直接是int&&
(右值引用)。因此,std::forward<T>(arg)
將arg
作為右值引用轉發給process
函式,呼叫process(int&& i)
版本。
#include <iostream>
#include <utility> // std::forward
// 分別處理左值和右值
void process(int& i) {
std::cout << "Process left value: " << i << std::endl;
}
void process(int&& i) {
std::cout << "Process right value: " << i << std::endl;
}
// 完美轉發的模板函式
template<typename T>
void wrapper(T&& arg) {
// 使用std::forward來完美轉發arg
process(std::forward<T>(arg));
}
int main() {
int lv = 10; // 左值
wrapper(lv); // arg被推斷為左值引用,因為lv是一個左值
wrapper(20); // 20是右值,arg被推斷為右值引用
return 0;
}
/*
Process left value: 10
Process right value: 20
*/
轉移(Move)
轉移是指將一個物件的資源(如動態記憶體)從一個例項轉移到另一個例項,而不是複製資源。這通常透過移動建構函式和移動賦值運算子實現,它們接受一個右值引用(Rvalue reference)作為引數。移動語義允許資源的高效轉移,避免了不必要的複製,特別是對於大型物件或資源密集型物件。
使用
std::move
方法可以將左值轉換為右值。使用這個函式並不能移動任何東西,而是和移動建構函式一樣都具有移動語義,將物件的狀態或者所有權從一個物件轉移到另一個物件,只是轉移,沒有記憶體複製。
從實現上講,
std::move
基本等同於一個型別轉換:static_cast<T&&>(lvalue);
,函式原型如下:
template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
聯絡
- 完美轉發和移動語義都緊密依賴於左值和右值的概念。完美轉發用於保持引數的左值或右值性質不變,而移動語義則是利用右值(通常是即將銷燬的臨時物件)來最佳化資源的管理。
- 移動語義是完美轉發常見的一個應用場景。當使用完美轉發將函式引數傳遞給另一個函式時,如果該引數是一個臨時物件(右值),則可以利用移動建構函式或移動賦值運算子,從而提高效率。
- 右值的概念是轉移語義的基礎。只有右值(臨時物件或顯式標記為右值的物件)才能被移動,以此來最佳化資源的使用和提高程式的執行效率。
示例
以下是一個簡單示例,其中包含一個自定義的String
類,這個類透過實現移動建構函式和移動賦值運算子來最佳化記憶體資源管理。
同時,程式碼使用完美轉發的函式模板,它可以根據傳入引數的型別(左值或右值)來決定是否使用移動語義。
#include <iostream>
#include <cstring>
#include <utility> // std::move and std::forward
class String {
private:
char* data;
size_t length;
void freeData() {
delete[] data;
}
public:
// 建構函式
String(const char* p = "") : length(strlen(p)), data(new char[length + 1]) {
std::copy(p, p + length + 1, data);
std::cout << "Constructed\n";
}
// 解構函式
~String() {
freeData();
}
// 複製建構函式
String(const String& other) : length(other.length), data(new char[length + 1]) {
std::copy(other.data, other.data + length + 1, data);
std::cout << "Copied\n";
}
// 移動建構函式
String(String&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
std::cout << "Moved\n";
}
// 移動語義的賦值運算子
String& operator=(String&& other) noexcept {
if (this != &other) {
freeData();
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move Assigned\n";
}
return *this;
}
void print() const {
if (data) {
std::cout << data << std::endl;
}
}
};
// 完美轉發示例
template<typename T>
void relay(T&& arg) {
// 使用 完全轉發 來保持'arg'的左值/右值性質。
String temp(std::forward<T>(arg));
temp.print();
}
int main() {
String s1("Hello");
String s2(std::move(s1)); // 呼叫移動建構函式
s1 = String("World"); // 移動語義賦值呼叫
String s3("Goodbye");
relay(s3); // 左值被傳遞
relay(String("Hello World")); // 右值被傳遞
return 0;
}
/*
Constructed
Moved
Constructed
Move Assigned
Constructed
Copied
Goodbye
Constructed
Moved
Hello World
*/
程式輸出:
Constructed
Moved
Constructed
Move Assigned
Constructed
Copied
Goodbye
Constructed
Moved
Hello World
String
類包含了移動建構函式和移動賦值運算子,當與右值互動時,可以有效地轉移資源而不是進行復制。這樣,當有一個臨時的String
物件時(例如在main
函式中透過String("World")
建立的臨時物件),這個物件的資源可以被轉移到另一個物件中而不需要額外的複製開銷。
參考
-
愛程式設計的大丙:轉移和完美轉發
-
愛程式設計的大丙:右值引用