C++ 右值語義相關(guān)總結(jié)
在現(xiàn)代C++的眾多特性中,右值語義(std::move和std::forward)大概是最神奇也最難懂的特性之一了。本文簡要介紹了現(xiàn)代C++中右值語義特性的原理和使用。
1 什么是左值,什么是右值?
int a = 0; // a是左值,0是右值 int b = rand(); // b是左值,rand()是右值
直觀理解:左值在等號左邊,右值在等號右邊
深入理解:左值有名稱,可根據(jù)左值獲取其內(nèi)存地址,而右值沒有名稱,不能根據(jù)右值獲取地址。
2 引用疊加規(guī)則
左值引用A&和右值引用A&&可相互疊加, 疊加規(guī)則如下:
A& + A& = A& A& + A&& = A& A&& + A& = A& A&& + A&& = A&&
舉例說明,在模板函數(shù)void foo(T&& x)中:
如果T是int&類型, T&&為int&,x為左值語義
如果T是int&&類型, T&&為int&&, x為右值語義
也就是說,不管輸入?yún)?shù)x為左值還是右值,都能傳入函數(shù)foo。區(qū)別在于兩種情況下,編譯器推導(dǎo)出模板參數(shù)T的類型不一樣。
3 std::move
3.1 What?
在C++11中引入了std::move函數(shù),用于實現(xiàn)移動語義。它用于將臨時變量(也有可能是左值)的內(nèi)容直接移動給被賦值的左值對象。
3.2 Why?
知道了std::move是干什么的,他能給我們的搬磚工作帶來哪些好處呢? 舉例說明:
如果類X包含一個指向某資源的指針,在左值語義下,類X的復(fù)制構(gòu)造函數(shù)定義如下:
X::X() { // 申請資源(指針表示) } X::X(const X& other) { // ... // 銷毀資源 // 克隆other中的資源 // ... } X::~X() { // 銷毀資源 }
假設(shè)應(yīng)用代碼如下。其中,對象tmp被賦給a之后,便不再使用。
X tmp; // ...經(jīng)過一系列初始化... X a = tmp;
在上面的代碼中,執(zhí)行步驟:
- 先執(zhí)行一次默認構(gòu)造函數(shù)(默認構(gòu)造tmp對象)
- 再執(zhí)行一次復(fù)制構(gòu)造函數(shù)(復(fù)制構(gòu)造a對象)
- 退出作用域時執(zhí)行析構(gòu)函數(shù)(析構(gòu)tmp和a對象)
從資源的視角來看,上述代碼中共執(zhí)行了2次資源申請和3次資源釋放。
那么問題來了,既然對象tmp只是一個臨時對象,在執(zhí)行X a = tmp;時,對象a能否將tmp的資源'偷'過來,直接為我所用,而不影響原來的功能? 答案是可以。
X::X(const X& other) { // 使用std::swap交換this和other的資源 }
通過'偷'對象tmp的資源,減少了資源申請和釋放的開銷。而std::swap交換指針代價極小,可忽略不計。
3.3 How?
到現(xiàn)在為止,我們明白了std::move將要達到的效果,那么它究竟是怎么實現(xiàn)的呢?
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); }
不管輸入?yún)?shù)為左值還是右值,都被remove_reference去掉其引用屬性,RvalRef為右值類型,最終返回類型為右值引用。
3.4 Example
在實際使用中,一般將臨時變量作為std::move的輸入?yún)?shù),并將返回值傳入接受右值類型的函數(shù)中,方便其'偷取'臨時變量中的資源。需要注意的是,臨時變量被'偷'了之后,便不能對其進行讀寫,否則會產(chǎn)生未定義行為。
#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復(fù)制構(gòu)造 b.push_back(std::move(a[1])); // 開銷:string移動構(gòu)造(將臨時變量a[1]中的指針偷過來) std::cout << "bsize: " << b.size() << std::endl; for (std::string& x: b) std::cout << x << std::endl; bar(); return 0; }
4 std::forward
4.1 What?
std::forward用于實現(xiàn)完美轉(zhuǎn)發(fā)。那么什么是完美轉(zhuǎn)發(fā)呢?完美轉(zhuǎn)發(fā)實現(xiàn)了參數(shù)在傳遞過程中保持其值屬性的功能,即若是左值,則傳遞之后仍然是左值,若是右值,則傳遞之后仍然是右值。
簡單來說,std::move用于將左值或右值對象強轉(zhuǎn)成右值語義,而std::forward用于保持左值對象的左值語義和右值對象的右值語義。
4.2 Why?
#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; }
執(zhí)行以上代碼會發(fā)現(xiàn),foo(x)和foo(10)都會輸出lvalue。foo(x)輸出lvalue可以理解,因為x是左值嘛,但是10是右值,為啥foo(10)也輸出lvalue呢?
這是因為10只是作為函數(shù)foo的右值參數(shù),但是在foo內(nèi)部,10被帶入了形參x,而x是一個有名字的變量,即右值,因此foo中bar(x)還是輸出lvalue。
那么問題來了,如果我們想在foo函數(shù)內(nèi)部保持x的右值語義,該怎么做呢?std::forward便派上了用場。
只需改寫foo函數(shù):
template <typename T> void foo(T&& x) { bar(std::forward<T>(x)); }
4.3 How?
std::forward聽起來有點神奇,那么它到底是如何實現(xiàn)的呢?
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的輸入?yún)?shù)是一個左值,那么Arg = X&,根據(jù)疊加規(guī)則,std::forward<Arg> = X&。因此,在這種情況下,std::forward<Arg>(arg)仍然是左值。
相反,如果factory輸入?yún)?shù)是一個右值,那么Arg = X,std::forward<Arg> = X。這種情況下,std::forward<Arg>(arg)是一個右值。
恰好達到了保留左值or右值語義的效果!
4.4 Example
直接上代碼。如果前面都懂了,相信這段代碼的輸出結(jié)果也能猜個八九不離十了。
#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; }
以上就是C++ 右值語義相關(guān)總結(jié)的詳細內(nèi)容,更多關(guān)于C++ 右值語義的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C/C++實現(xiàn)磁盤相關(guān)操作的示例代碼
這篇文章主要為大家詳細介紹了C/C++如何實現(xiàn)磁盤相關(guān)操作,例如遍歷磁盤容量、實現(xiàn)磁盤格式化、移除指定磁盤等,感興趣的小伙伴可以跟隨小編一起學習一下2023-11-11VC程序設(shè)計中CreateProcess用法注意事項
這篇文章主要介紹了VC程序設(shè)計中CreateProcess用法注意事項,需要的朋友可以參考下2014-07-07C++利用數(shù)組(一維/二維)處理批量數(shù)據(jù)的方法
對于簡單的問題,使用簡單的數(shù)據(jù)類型就可以了,但是對于有些需要處理的數(shù)據(jù),只用以上簡單的數(shù)據(jù)類型是不夠的,難以反映出數(shù)據(jù)的特點,也難以有效的進行處理,本文小編給大家介紹了C++利用數(shù)組(一維/二維)處理批量數(shù)據(jù)的方法,需要的朋友可以參考下2023-10-10