1. 左值、右值、左值引用以及右值引用 左值:一般指的是在記憶體中有對應的存儲單元的值,最常見的就是程式中創建的變數 右值:和左值相反,一般指的是沒有對應存儲單元的值(寄存器中的立即數,中間結果等),例如一個常量,或者表達式計算的臨時變數 int x = 10 int y = 20 int z = x ...
1. 左值、右值、左值引用以及右值引用
- 左值:一般指的是在記憶體中有對應的存儲單元的值,最常見的就是程式中創建的變數
- 右值:和左值相反,一般指的是沒有對應存儲單元的值(寄存器中的立即數,中間結果等),例如一個常量,或者表達式計算的臨時變數
int x = 10 int y = 20 int z = x + y //x, y , z 是左值 //10 , 20,x + y 是右值,因為它們在完成賦值操作後即消失,沒有占用任何資源
- 左值引用:C++中採用 &對變數進行引用,這種常規的引用就是左值引用
- 右值引用:右值引用最大的作用就是讓一個左值達到類似右值的效果(下麵程式舉例),讓變數之間的轉移更符合“語義上的轉移”,以減少轉移之間多次拷貝的開銷。右值引用符號是&&。
例如,對於以下程式,我們要將字元串放到vector中,且我們後續的代碼中不再用到x:
std::vector<std::string> vec; std::string x = "abcd"; vec.push_back(x); std::cout<<"x: "<<x<<"\n"; std::cout<<"vector: "<< vec[0]<<"\n"; //-------------output------------------ // x: abcd // vector: abcd
該程式在真正執行的過程中,實際上是複製了一份字元串x,將其放在vector中,這其中多了一個拷貝的開銷和記憶體上的開銷。但如果x以及沒有作用了,我們希望做到的是 真正的轉移,即x指向的字元串移動到vector中,不需要額外的記憶體開銷和拷貝開銷。因此我們希望讓變數 x傳入到push_back 表現的像一個右值 ,這個時候就體現右值引用的作用,只需要將x
的右值引用傳入就可以。
改進成如下代碼:
std::vector<std::string> vec; std::string x = "abcd"; vec.push_back(std::move(x)); <--------------- 使用了std::move,任何的左值/右值通過std::move都轉化為右值引用 std::cout<<"x: "<<x<<"\n"; std::cout<<"vector: "<< vec[0]<<"\n"; //-------------output------------------ // x: // vector: abcd
可以看到,完成`push_back`後x
是空的。
2. 移動語義
移動語義是通過移動構造和移動賦值避免無意義的拷貝操作。
2.1 使用std::move實現移動構造
定義:採用右值引用作為參數的構造函數又稱作移動構造函數。此時不需要額外的拷貝操作,也不需要新分配記憶體。
使用場景:對於一個值(比如數組、字元串、對象等)如果在執行某個操作後不再使用,那麼這個值就叫做將亡值(Expiring Value),因此對於本次操作我們就沒必要對該值進行額外的拷貝操作,而是希望直接轉移,儘可能減少額外的拷貝開銷,操作後該值也不再占用額外的資源。
使用函數:std::move,任何的左值/右值通過std::move都轉化為右值引用
看如下例子,
#include <iostream> #include <vector> #include <string> class A { public: A(){} A(size_t size): size(size), array((int*) malloc(size)) { std::cout << "create Array,memory at: " << array << std::endl; } ~A() { free(array); } A(A &&a) : array(a.array), size(a.size) { a.array = nullptr; std::cout << "Array moved, memory at: " << array << std::endl; } A(A &a) : size(a.size) { array = (int*) malloc(a.size); for(int i = 0;i < a.size;i++) array[i] = a.array[i]; std::cout << "Array copied, memory at: " << array << std::endl; } size_t size; int *array; }; int main() { std::vector<A> vec; A a = A(10); vec.push_back(a); return 0; } //----------------output-------------------- // create Array,memory at: 0x600002a28030 // A a = A(10); 調用了 構造函數A(size_t size){} // Array copied, memory at: 0x600002a28050 //vec push的時候拷貝一份,調用構造函數A(A &a){}
從輸出可以看到,每次進行push_back的時候,會重新創建一個對象,調用了左值引用A(A &a) : size(a.size)
對應的構造函數,將對象中的數組重新深拷貝一份。
如果該用右值引用進行優化,如下
int main () { std::vector<A> vec; A a = A(10); vec.push_back(std::move(a)); return 0; } //----------------output-------------------- // create Array,memory at: 0x600003a84030 // Array moved, memory at: 0x600003a84030
可以看到,這個時候雖然也重新創建了一個對象,但是調用的是這個構造函數A(A &&a) : array(a.array), size(a.size)
(這種採用右值引用作為參數的構造函數又稱作移動構造函數),此時不需要額外的拷貝操作,也不需要新分配記憶體。
3. 完美轉發
使用函數:std::forward,如果傳遞的是左值轉發的就是左值引用,傳遞的是右值轉發的就是右值引用。
3.1 引用摺疊
在具體介紹std::forward之前,需要先瞭解C++的引用摺疊規則,對於一個值引用的引用最終都會被摺疊成左值引用或者右值引用。
- T& & -> T& (對左值引用的左值引用是左值引用)
- T& && -> T& (對左值引用的右值引用是左值引用)
- T&& & ->T& (對右值引用的左值引用是左值引用)
- T&& && ->T&& (對右值引用的右值引用是右值引用)
總結一句話,只有對於右值引用的右值引用摺疊完還是右值引用,其他都會被摺疊成左值引用。
3.2 使用std::forward實現完美轉發
std::forward的作用就是完美轉發,確保轉發過程中引用的類型不發生任何改變,左值引用轉發後一定還是左值引用,右值引用轉發後一定還是右值引用!
下麵是一個使用 std::forward 的例子:
#include <iostream> #include <utility> void func(int& x) { std::cout << "lvalue reference: " << x << std::endl; } void func(int&& x) { std::cout << "rvalue reference: " << x << std::endl; } template<typename T> void wrapper(T&& arg) { func(std::forward<T>(arg)); } int main() { int x = 42; wrapper(x); // lvalue reference: 42 wrapper(1); // rvalue reference: 1 return 0; }
在上面的例子中,我們定義了兩個函數 func,一個接受左值引用,另一個接受右值引用。然後我們定義了一個模板函數 wrapper,在 wrapper 函數中,我們使用 std::forward 函數將參數 arg 轉發給 func 函數。通過使用 std::forward,我們可以確保 func 函數接收到的參數的左右值特性與原始參數保持一致。
- 當向wrapper裡面傳入x的時候,wrapper推導認為 T是一個左值引用int &,通過引用摺疊原則(看萬能引用文章)int && + & = int &,相當於wrapper(int& arg),同時我們知道了T推導為int&,那麼在向func傳遞的時候,就是func(std::forward<int&> (arg)) ,那麼func會以左值引用的形式 func(int& x) 調用arg。
- 當向wrapper裡面傳入1的時候,wrapper推導認為T是一個右值引用int&& ,通過引用摺疊原則,int && + && =int&& ,相當於wrapper(int&& arg),同時我們知道了T推導為int&&,那麼在向func傳遞的時候,就是func(std::forward<int&&>(arg)),那麼func會以左值引用的形式func(int&& x)調用arg。
另一個例子:
class Test{}; void B(Test& a) {cout << "B&" << endl;} void B(Test&& a) {cout << "B&&" << endl;} template<typename T> void A(T &&a) { B(std::forward<T>(a)); } int main() { Test a; A(std::move(a)); A(a); return 0; } ////// //輸出結果 B&& B&
參考鏈接:https://zhuanlan.zhihu.com/p/469607144
https://www.jb51.net/article/278300.htm