更多精彩內容,請關注微信公衆號:後端技術小屋ios
在現代C++的衆多特性中,右值語義(std::move和std::forward)大概是最神奇也最難懂的特性之一了。本文簡要介紹了現代C++中右值語義特性的原理和使用。redis
int a = 0; // a是左值,0是右值 int b = rand(); // b是左值,rand()是右值
直觀理解:左值在等號左邊,右值在等號右邊後端
深刻理解:左值有名稱,可根據左值獲取其內存地址,而右值沒有名稱,不能根據右值獲取地址。微信
左值引用A&
和右值引用A&&
可相互疊加, 疊加規則以下:分佈式
A& + A& = A& A& + A&& = A& A&& + A& = A& A&& + A&& = A&&
舉例說明,在模板函數void foo(T&& x)
中:函數
T
是int&
類型, T&&
爲int&
,x
爲左值語義T
是int&&
類型, T&&
爲int&&
, x
爲右值語義也就是說,無論輸入參數x
爲左值仍是右值,都能傳入函數foo
。區別在於兩種狀況下,編譯器推導出模板參數T
的類型不同。源碼分析
在C++11中引入了std::move
函數,用於實現移動語義
。它用於將臨時變量(也有多是左值)的內容直接移動給被賦值的左值對象。this
知道了std::move
是幹什麼的,他能給咱們的搬磚工做帶來哪些好處呢? 舉例說明:指針
若是類X包含一個指向某資源的指針,在左值語義下,類X的複製構造函數定義以下:rest
X::X() { // 申請資源(指針表示) } X::X(const X& other) { // ... // 銷燬資源 // 克隆other中的資源 // ... } X::~X() { // 銷燬資源 }
假設應用代碼以下。其中,對象tmp被賦給a以後,便再也不使用。
X tmp; // ...通過一系列初始化... X a = tmp;
在上面的代碼中,執行步驟:
從資源的視角來看,上述代碼中共執行了2次資源申請和3次資源釋放。
那麼問題來了,既然對象tmp只是一個臨時對象,在執行X a = tmp;
時,對象a
可否將tmp
的資源'偷'過來,直接爲我所用,而不影響原來的功能? 答案是能夠。
X::X(const X& other) { // 使用std::swap交換this和other的資源 }
經過'偷'對象tmp的資源,減小了資源申請和釋放的開銷。而std::swap
交換指針代價極小,可忽略不計。
到如今爲止,咱們明白了std::move
將要達到的效果,那麼它到底是怎麼實現的呢?
template<class T> typename remove_reference<T>::type&& std::move(T&& a) noexcept { typedef typename remove_reference<T>::type&& RvalRef; return static_cast<RvalRef>(a); }
無論輸入參數爲左值仍是右值,都被remove_reference
去掉其引用屬性,RvalRef
爲右值類型,最終返回類型爲右值引用。
在實際使用中,通常將臨時變量做爲std::move
的輸入參數,並將返回值傳入接受右值類型的函數中,方便其'偷取'臨時變量中的資源。須要注意的是,臨時變量被'偷'了以後,便不能對其進行讀寫,不然會產生未定義行爲。
#include <utility> #include <iostream> #include <string> #include <vector> void foo(const std::string& n) { std::cout << "lvalue" << std::endl; } void foo(std::string&& n) { std::cout << "rvalue" << std::endl; } void bar() { foo("hello"); // rvalue std::string a = "world"; foo(a); // lvalue foo(std::move(a)); // rvalue } int main() { std::vector<std::string> a = {"hello", "world"}; std::vector<std::string> b; b.push_back("hello"); // 開銷:string複製構造 b.push_back(std::move(a[1])); // 開銷:string移動構造(將臨時變量a[1]中的指針偷過來) std::cout << "bsize: " << b.size() << std::endl; for (std::string& x: b) std::cout << x << std::endl; bar(); return 0; }
std::forward
用於實現完美轉發。那麼什麼是完美轉發呢?完美轉發實現了參數在傳遞過程當中保持其值屬性的功能,即如果左值,則傳遞以後仍然是左值,如果右值,則傳遞以後仍然是右值。
簡單來講,std::move
用於將左值或右值對象強轉成右值語義,而std::forward
用於保持左值對象的左值語義和右值對象的右值語義。
#include <utility> #include <iostream> void bar(const int& x) { std::cout << "lvalue" << std::endl; } void bar(int&& x) { std::cout << "rvalue" << std::endl; } template <typename T> void foo(T&& x) { bar(x); } int main() { int x = 10; foo(x); // 輸出:lvalue foo(10); // 輸出:lvalue return 0; }
執行以上代碼會發現,foo(x)
和foo(10)
都會輸出lvalue
。foo(x)
輸出lvalue
能夠理解,由於x
是左值嘛,可是10
是右值,爲啥foo(10)
也輸出lvalue
呢?
這是由於10
只是做爲函數foo
的右值參數,可是在foo
內部,10
被帶入了形參x
,而x
是一個有名字的變量,即右值,所以foo
中bar(x)
仍是輸出lvalue
。
那麼問題來了,若是咱們想在foo
函數內部保持x
的右值語義,該怎麼作呢?std::forward
便派上了用場。
只需改寫foo
函數:
template <typename T> void foo(T&& x) { bar(std::forward<T>(x)); }
std::forward
聽起來有點神奇,那麼它究竟是如何實現的呢?
template<typename T, typename Arg> shared_ptr<T> factory(Arg&& arg) { return shared_ptr<T>(new T(std::forward<Arg>(arg))); } template<class S> S&& forward(typename remove_reference<S>::type& a) noexcept { return static_cast<S&&>(a); } X x; factory<A>(x);
若是factory
的輸入參數是一個左值,那麼Arg = X&
,根據疊加規則,std::forward<Arg> = X&
。所以,在這種狀況下,std::forward<Arg>(arg)
仍然是左值。
相反,若是factory輸入參數是一個右值,那麼Arg = X
,std::forward<Arg> = X
。這種狀況下,std::forward<Arg>(arg)
是一個右值。
剛好達到了保留左值or右值語義的效果!
直接上代碼。若是前面都懂了,相信這段代碼的輸出結果也能猜個八九不離十了。
#include <utility> #include <iostream> void overloaded(const int& x) { std::cout << "[lvalue]" << std::endl; } void overloaded(int&& x) { std::cout << "[rvalue]" << std::endl; } template <class T> void fn(T&& x) { overloaded(x); overloaded(std::forward<T>(x)); } int main() { int i = 10; overloaded(std::forward<int>(i)); overloaded(std::forward<int&>(i)); overloaded(std::forward<int&&>(i)); fn(i); fn(std::move(i)); return 0; }
推薦閱讀
更多精彩內容,請掃碼關注微信公衆號:後端技術小屋。若是以爲文章對你有幫助的話,請多多分享、轉發、在看。