〇、前言
本文會討論C++中的左值,右值,左值引用,右值引用,以及會理清它們之間的關系。
一、左值與右值
(一)概述
1. 左值是一般指表達式結束后依然存在的持久化對象。右值指表達式結束時就不再存在的臨時對象。便捷的判斷方法:能對表達式取地址、有名字的對象為左值。反之,不能取地址、匿名的對象為右值。
2. C++ 表達式(運算符帶上其操作數、字面量、變量名等)有兩種獨立的屬性:類型和值類別 。類型指變量聲明時的類型,而值類別是表達式結果的類型,它必屬于左值、純右值或將亡值三者之一。如int&& x;
其中 x 的類型為右值引用,但作為表達式使用時其值類別為左值(因為有名字,可以取址)。比如:
#include <iostream>
void func(int &&b) {std::cout << "b: " << b << " addr: " << &b << std::endl;b++;std::cout << b << std::endl;
}
int main() {int c = 10;func(std::move(c));std::cout << "c: " << c << " addr: " << &c << std::endl;return 0;
}
上例中的 b 就是一個右值引用,它接受了一個右值 c。在 main()
中,c 被轉化為右值引用傳入 func()
,因此,func()
中的 b 其實就是 main()
中的 c 別名,它們指向的是同一塊內存區域。需要注意的是,func()
中的 b 其實是一個左值,換句話說,右值引用如果綁定了一個右值,它會延長這個右值的生命周期。 這種生命周期的延長意味著,盡管原始表達式產生的值是一個右值,一旦它被一個右值引用所綁定,它就不再是一個"即將銷毀的臨時值",而更像是一個普通的變量。 這允許開發者在保證效率的同時,也能夠更靈活地控制這些值。
運行結果:
g++ 1.cxx -o main -std=c++14
./main
b: 10 addr: 0x16ba16e28
11
c: 11 addr: 0x16ba16e28
我們需要注意的是,main()
中的 c 被轉換成了右值引用,但是它的狀態沒有發生任何改變。它在 func 中被修改,并在 func 返回后仍然保持有效。std::move
只是告訴編譯器一個對象可以被“安全地”當作右值來使用,這樣就允許在移動語義上下文中使用該對象。
(二)右值的分類
1. 純右值(prvalue):用于識別臨時變量和一些不與對象關聯的值。函數返回值為非引用類型、表達式臨時值(如1+3)、lambda表達式等。
2. 將亡值(xvalue):是與右值引用相關的表達式,通常指將要被移動的對象。如,函數返回類型為T&&
、std::move
的返回值、轉換為T&&
的類型轉換函數的返回值(注意,這些都是與右值引用相關的表達式)或臨時對象。
表格形式:
類型分類 | 特征 | 綁定規則和類型安全轉換 | 實例和特殊情況 |
---|---|---|---|
左值 (lvalue) | 存在變量、函數調用產生的對象,有固定地址。 | 可綁定到左值引用,例如 int& x = a; | 變量 int a = 10; 函數調用(返回引用)例如 int& foo(); |
純右值 (prvalue) | 不適宜被移動,不具有可識別的地址。 | 可綁定到右值引用或 const 左值引用,例如 int&& x = 5; | 字面量例如 42 ,表達式例如 a + b ,函數調用(返回非引用)例如 int foo(); |
將亡值 (xvalue) | 適宜被移動,具有地址但對象即將被銷毀。 | 可綁定到右值引用,例如使用 std::move 例如 int&& x = std::move(a); | 通過 std::move 生成的將亡值,例如 std::move(a) ;使用移動構造函數或移動賦值操作時,傳遞的實際參數。 |
泛左值 (glvalue) | 概括左值和將亡值,即擁有特定身份(地址)且可能即將被銷毀。 | 可以綁定到左值引用或右值引用,取決于上下文。 | 例如使用 decltype 捕獲引用時,decltype((a)) x = a; 捕獲的是 a 的引用。 |
二、左值引用與右值引用
(一)概述
1. 左值引用和右值引用都屬于引用類型。無論是聲明一個左值引用還是右值引用都必須立即進行初始化(今天考試剛考到)。
2. 左值引用都是左值。但具名的右值引用是左值,而匿名的右值引用是右值(比如上面的 func 中的 b,雖然它是右值引用,但是它是一個左值)。
(二)可綁定的值類型(設T是個具體類型)
1. 左值引用(T&):只能綁定到左值(非常量左值)
2. 右值引用(T&&):只能綁定到右值(非常量右值)
3. 常量左值引用(const T&
):常量左值引用是個“萬能”的引用類型:它既可以綁定到左值也可以綁定到右值,它像右值引用一樣可以延長右值的生命期。不過相比于右值引用所引用的右值,常量左值引用的右值在它的“余生”中只能是只讀的。對于這點,可以參考這個例子:
#include <iostream>
void func(const int &b) {std::cout << "b: " << b << " addr: " << &b << std::endl;b++; // 會報錯std::cout << b << std::endl;
}
int main() {int c = 10;func(std::move(c));std::cout << "c: " << c << " addr: " << &c << std::endl;return 0;
}
編譯出錯:
g++ 2.cxx -o main -std=c++14
2.cxx:4:6: error: cannot assign to variable 'b' with const-qualified type 'const int &'b++;~^
2.cxx:2:22: note: variable 'b' declared const here
void func(const int &b) {~~~~~~~~~~~^
1 error generated.
4. 常量右值引用(const T&&
):可綁定到右值或常量右值。由于移動語義需要右值可以被修改,因此常量右值引用沒有實際用處。如果需要引用右值且讓其不可更改,則常量左值引用就足夠了。
三、萬能引用(universal reference)
(一)T&&的含義
1. 當T是一個具體的類型時,T&&
表示右值引用,只能綁定到右值。
2. 當涉及T類型推導時,T&&
為萬能引用。若用右值初始化萬能引用,則T&&為右值引用。若用左值初始化萬能引用,則T&&為左值引用。但不管哪種情況,T&&都是一種引用類型。
(二)萬能引用
1. T&&
是萬能引用的兩個條件:
(1)必須涉及類型推導;
(2)聲明的形式也必須正好形如 T&&
。并且該形式被限定死了,任何對其修飾都將剝奪T&&成為萬能引用的資格。
2. 萬能引用使用的場景
(1)函數模板形參
(2)auto&&
一個例子:
#include <iostream>
#include <vector>using namespace std;class Widget {};void func1(Widget &¶m){}; // param為右值引用類型(不涉及類型推導)template <typename T>
void func2(T &¶m) {} // param為萬能引用(涉及類型推導)template <typename T>
void func3(std::vector<T> &¶m) {
}template <typename T>
void func4(const T &¶m) {}
template <class T> class MyVector {public:void push_back(T &&x) {} // x為右值引用。因為當定義一個MyVector對象后,T己確定。當調用該函數時T的類型不用再推導!template <class... Args>void emplace_back(Args&&...args){}; // args為萬能引用,因為Args獨立于T的類型,當調用該函數時,需推導Args的類型。
};int main() {Widget w;func2(w); // 萬能引用, func2(T&& param),param為Widget&(左值引用)func2(std::move(w)); // 萬能引用, param為Widget&&,是個右值引用。int x = 0;Widget &&var1 = Widget(); // var1為右值引用(不涉及類型推導)auto &&var2 = var1; //萬能引用,auto&&被推導為Widget&(左值引用)auto &&var3 = x; //萬能引用,被推導為int&;(左值引用)// 3. 計算任意函數的執行時間:auto&&用于lambda表達式形參(C++14)auto timefunc = [](auto &&func, auto &&...params) {//計時器啟動//調用func(param...)函數std::forward<decltype(func)>(func)( //根據func的左右值特性來調用相應的重載&或&&版本的成員函數std::forward<decltype(params)>(params)... //保持參數的左/右值特性);//計時器停止并記錄流逝的時間};timefunc(func1, std::move(w)); //計算func1函數的執行時間return 0;
}
在你提供的代碼中,main 函數涉及到萬能引用的語句如下:
-
func2(w);
和func2(std::move(w));
— 這里的func2
函數模板參數T&&
是一個萬能引用。它可以綁定到左值和右值。在這兩個調用中,第一次調用時T
被推導為Widget&
(因為w
是一個左值),而第二次調用時T
被推導為Widget
(因為std::move(w)
產生一個右值)。 -
auto &&var2 = var1;
— 這里使用了auto&&
,它也是一個萬能引用。var1
是一個左值(盡管它本身是一個綁定到臨時對象的右值引用),所以var2
被推導為Widget&
。 -
auto &&var3 = x;
— 這同樣使用了auto&&
,這是一個萬能引用。由于x
是一個左值,var3
被推導為int&
。 -
在
timefunc
lambda 表達式的定義中,參數列表(auto &&func, auto &&...params)
使用了萬能引用。這里auto&&
用于單個參數和參數包,允許這個 lambda 接受任意數量的任意類型的參數,并保持他們的值類別(左值或右值)。
這些例子展示了萬能引用在模板類型推導中的強大功能,尤其是在泛型編程和函數重載解析中的應用。通過萬能引用,可以寫出更靈活的函數和模板,使得它們能夠同時接受左值和右值,而無需重載函數。
四、參考
這里。