您好,登錄后才能下訂單哦!
這篇文章主要講解了“C++右值引用與移動構造函數應用的方法是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“C++右值引用與移動構造函數應用的方法是什么”吧!
右值引用是 C++11 引入的與 Lambda 表達式齊名的重要特性之一。它的引入解決了 C++ 中大量的歷史遺留問題, 消除了諸如 std::vector、std::string 之類的額外開銷, 也才使得函數對象容器 std::function 成為了可能。
要弄明白右值引用到底是怎么一回事,必須要對左值和右值做一個明確的理解。
左值 (lvalue, left value),顧名思義就是賦值符號左邊的值。準確來說, 左值是表達式(不一定是賦值表達式)后依然存在的持久對象。
右值 (rvalue, right value),右邊的值,是指表達式結束后就不再存在的臨時對象。
而 C++11 中為了引入強大的右值引用,將右值的概念進行了進一步的劃分,分為:純右值、將亡值。
純右值 (prvalue, pure rvalue),純粹的右值,要么是純粹的字面量,例如 10, true; 要么是求值結果相當于字面量或匿名臨時對象,例如 1+2。非引用返回的臨時變量、運算表達式產生的臨時變量、 原始字面量、Lambda 表達式都屬于純右值。
需要注意的是,字面量除了字符串字面量以外,均為純右值。而字符串字面量是一個左值,類型為 const char 數組。例如:
#include <type_traits> int main() { // 正確,"01234" 類型為 const char [6],因此是左值 const char (&left)[6] = "01234"; // 斷言正確,確實是 const char [6] 類型,注意 decltype(expr) 在 expr 是左值 // 且非無括號包裹的 id 表達式與類成員表達式時,會返回左值引用 static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, ""); // 錯誤,"01234" 是左值,不可被右值引用 // const char (&&right)[6] = "01234"; }
但是注意,數組可以被隱式轉換成相對應的指針類型,而轉換表達式的結果(如果不是左值引用)則一定是個右值(右值引用為將亡值,否則為純右值)。例如:
const char* p = "01234"; // 正確,"01234" 被隱式轉換為 const char* const char*&& pr = "01234"; // 正確,"01234" 被隱式轉換為 const char*,該轉換的結果是純右值 // const char*& pl = "01234"; // 錯誤,此處不存在 const char* 類型的左值 將亡值 (xvalue, expiring value),是 C++11 為了引入右值引用而提出的概念(因此在傳統 C++ 中, 純右值和右值是同一個概念),也就是即將被銷毀、卻能夠被移動的值。 將亡值可能稍有些難以理解,我們來看這樣的代碼: std::vector<int> foo() { std::vector<int> temp = {1, 2, 3, 4}; return temp; } std::vector<int> v = foo();
在這樣的代碼中,就傳統的理解而言,函數 foo 的返回值 temp 在內部創建然后被賦值給 v, 然而 v 獲得這個對象時,會將整個 temp 拷貝一份,然后把 temp 銷毀,如果這個 temp 非常大, 這將造成大量額外的開銷(這也就是傳統 C++ 一直被詬病的問題)。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是純右值)。但是,v 可以被別的變量捕獲到, 而 foo() 產生的那個返回值作為一個臨時值,一旦被 v 復制后,將立即被銷毀,無法獲取、也不能修改。 而將亡值就定義了這樣一種行為:臨時的值能夠被識別、同時又能夠被移動。
在 C++11 之后,編譯器為我們做了一些工作,此處的左值 temp 會被進行此隱式右值轉換, 等價于 static_cast<std::vector<int> &&>(temp),進而此處的 v 會將 foo 局部返回的值進行移動。 也就是后面我們將會提到的移動語義。
要拿到一個將亡值,就需要用到右值引用:T &&,其中 T 是類型。 右值引用的聲明讓這個臨時值的生命周期得以延長、只要變量還活著,那么將亡值將繼續存活。
C++11 提供了 std::move 這個方法將左值參數無條件的轉換為右值, 有了它我們就能夠方便的獲得一個右值臨時對象,例如:
#include <iostream> #include <string> void reference(std::string& str) { std::cout << "左值" << std::endl; } void reference(std::string&& str) { std::cout << "右值" << std::endl; } int main() { std::string lv1 = "string,"; // lv1 是一個左值 // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值 std::string&& rv1 = std::move(lv1); // 合法, std::move可以將左值轉移為右值 std::cout << rv1 << std::endl; // string, const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能夠延長臨時變量的生命周期 // lv2 += "Test"; // 非法, 常量引用無法被修改 std::cout << lv2 << std::endl; // string,string, std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延長臨時對象生命周期 rv2 += "Test"; // 合法, 非常量引用能夠修改臨時變量 std::cout << rv2 << std::endl; // string,string,string,Test reference(rv2); // 輸出左值 return 0; }
rv2 雖然引用了一個右值,但由于它是一個引用,所以 rv2 依然是一個左值。
注意,這里有一個很有趣的歷史遺留問題,我們先看下面的代碼:
#include <iostream> int main() { // int &a = std::move(1); // 不合法,非常量左引用無法引用右值 const int &b = std::move(1); // 合法, 常量左引用允許引用右值 std::cout << a << b << std::endl; } 第一個問題,為什么不允許非常量引用綁定到非左值?這是因為這種做法存在邏輯錯誤: void increase(int & v) { v++; } void foo() { double s = 1; increase(s); }
由于 int& 不能引用 double 類型的參數,因此必須產生一個臨時值來保存 s 的值, 從而當 increase() 修改這個臨時值時,調用完成后 s 本身并沒有被修改。
第二個問題,為什么常量引用允許綁定到非左值?原因很簡單,因為 Fortran 需要。
傳統 C++ 通過拷貝構造函數和賦值操作符為類對象設計了拷貝/復制的概念,但為了實現對資源的移動操作, 調用者必須使用先復制、再析構的方式,否則就需要自己實現移動對象的接口。 試想,搬家的時候是把家里的東西直接搬到新家去,而不是將所有東西復制一份(重買)再放到新家、 再把原來的東西全部扔掉(銷毀)
傳統的 C++ 沒有區分『移動』和『拷貝』的概念,造成了大量的數據拷貝,浪費時間和空間。 右值引用的出現恰好就解決了這兩個概念的混淆問題,例如:
#include <iostream> class A { public: int *pointer; A():pointer(new int(1)) { std::cout << "構造" << pointer << std::endl; } A(A& a):pointer(new int(*a.pointer)) { std::cout << "拷貝" << pointer << std::endl; } // 無意義的對象拷貝 A(A&& a):pointer(a.pointer) { a.pointer = nullptr; std::cout << "移動" << pointer << std::endl; } ~A(){ std::cout << "析構" << pointer << std::endl; delete pointer; } }; // 防止編譯器優化 A return_rvalue(bool test) { A a,b; if(test) return a; // 等價于 static_cast<A&&>(a); else return b; // 等價于 static_cast<A&&>(b); } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; }
在上面的代碼中:
首先會在 return_rvalue 內部構造兩個 A 對象,于是獲得兩個構造函數的輸出;
函數返回后,產生一個將亡值,被 A 的移動構造(A(A&&))引用,從而延長生命周期,并將這個右值中的指針拿到,保存到了 obj 中,而將亡值的指針被設置為 nullptr,防止了這塊內存區域被銷毀。
從而避免了無意義的拷貝構造,加強了性能。再來看看涉及標準庫的例子:
#include <iostream> // std::cout #include <utility> // std::move #include <vector> // std::vector #include <string> // std::string int main() { std::string str = "Hello world."; std::vector<std::string> v; // 將使用 push_back(const T&), 即產生拷貝行為 v.push_back(str); // 將輸出 "str: Hello world." std::cout << "str: " << str << std::endl; // 將使用 push_back(const T&&), 不會出現拷貝行為 // 而整個字符串會被移動到 vector 中,所以有時候 std::move 會用來減少拷貝出現的開銷 // 這步操作后, str 中的值會變為空 v.push_back(std::move(str)); // 將輸出 "str: " std::cout << "str: " << str << std::endl; return 0; }
前面我們提到了,一個聲明的右值引用其實是一個左值。這就為我們進行參數轉發(傳遞)造成了問題:
void reference(int& v) { std::cout << "左值" << std::endl; } void reference(int&& v) { std::cout << "右值" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通傳參:"; reference(v); // 始終調用 reference(int&) } int main() { std::cout << "傳遞右值:" << std::endl; pass(1); // 1是右值, 但輸出是左值 std::cout << "傳遞左值:" << std::endl; int l = 1; pass(l); // l 是左值, 輸出左值 return 0; }
對于 pass(1) 來說,雖然傳遞的是右值,但由于 v 是一個引用,所以同時也是左值。 因此 reference(v) 會調用 reference(int&),輸出『左值』。 而對于pass(l)而言,l是一個左值,為什么會成功傳遞給 pass(T&&) 呢?
這是基于引用坍縮規則的:在傳統 C++ 中,我們不能夠對一個引用類型繼續進行引用, 但 C++ 由于右值引用的出現而放寬了這一做法,從而產生了引用坍縮規則,允許我們對引用進行引用, 既能左引用,又能右引用。但是卻遵循如下規則:
函數形參類型 | 實參參數類型 | 推導后函數形參類型 |
T& | 左引用 | T& |
T& | 右引用 | T& |
T&& | 左引用 | T& |
T&& | 右引用 | T&& |
因此,模板函數中使用 T&& 不一定能進行右值引用,當傳入左值時,此函數的引用將被推導為左值。 更準確的講,無論模板參數是什么類型的引用,當且僅當實參類型為右引用時,模板參數才能被推導為右引用類型。 這才使得 v 作為左值的成功傳遞。
完美轉發就是基于上述規律產生的。所謂完美轉發,就是為了讓我們在傳遞參數的時候, 保持原來的參數類型(左引用保持左引用,右引用保持右引用)。 為了解決這個問題,我們應該使用 std::forward 來進行參數的轉發(傳遞):
#include <iostream> #include <utility> void reference(int& v) { std::cout << "左值引用" << std::endl; } void reference(int&& v) { std::cout << "右值引用" << std::endl; } template <typename T> void pass(T&& v) { std::cout << " 普通傳參: "; reference(v); std::cout << " std::move 傳參: "; reference(std::move(v)); std::cout << " std::forward 傳參: "; reference(std::forward<T>(v)); std::cout << "static_cast<T&&> 傳參: "; reference(static_cast<T&&>(v)); } int main() { std::cout << "傳遞右值:" << std::endl; pass(1); std::cout << "傳遞左值:" << std::endl; int v = 1; pass(v); return 0; }
輸出結果為:
傳遞右值:
普通傳參: 左值引用
std::move 傳參: 右值引用
std::forward 傳參: 右值引用
static_cast<T&&> 傳參: 右值引用
傳遞左值:
普通傳參: 左值引用
std::move 傳參: 右值引用
std::forward 傳參: 左值引用
static_cast<T&&> 傳參: 左值引用
無論傳遞參數為左值還是右值,普通傳參都會將參數作為左值進行轉發, 所以 std::move 總會接受到一個左值,從而轉發調用了reference(int&&) 輸出右值引用。
唯獨 std::forward 即沒有造成任何多余的拷貝,同時完美轉發(傳遞)了函數的實參給了內部調用的其他函數。
std::forward 和 std::move 一樣,沒有做任何事情,std::move 單純的將左值轉化為右值, std::forward 也只是單純的將參數做了一個類型的轉換,從現象上來看, std::forward<T>(v) 和 static_cast<T&&>(v) 是完全一樣的。
讀者可能會好奇,為何一條語句能夠針對兩種類型的返回對應的值, 我們再簡單看一看 std::forward 的具體實現機制,std::forward 包含兩個重載:
template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); } template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept { static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument" " substituting _Tp is an lvalue reference type"); return static_cast<_Tp&&>(__t); }
在這份實現中,std::remove_reference 的功能是消除類型中的引用, std::is_lvalue_reference 則用于檢查類型推導是否正確,在 std::forward 的第二個實現中 檢查了接收到的值確實是一個左值,進而體現了坍縮規則。
當 std::forward 接受左值時,_Tp 被推導為左值,所以返回值為左值;而當其接受右值時, _Tp 被推導為 右值引用,則基于坍縮規則,返回值便成為了 && + && 的右值。 可見 std::forward 的原理在于巧妙的利用了模板類型推導中產生的差異。
這時我們能回答這樣一個問題:為什么在使用循環語句的過程中,auto&& 是最安全的方式? 因為當 auto 被推導為不同的左右引用時,與 && 的坍縮組合是完美轉發。
感謝各位的閱讀,以上就是“C++右值引用與移動構造函數應用的方法是什么”的內容了,經過本文的學習后,相信大家對C++右值引用與移動構造函數應用的方法是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。