一文搞懂c++中的std::move函數(shù)
前言
在探討c++11中的Move函數(shù)前,先介紹兩個概念(左值和右值)
左值和右值
首先區(qū)分左值和右值
左值是表達式結(jié)束后依然存在的持久對象(代表一個在內(nèi)存中占有確定位置的對象)
右值是表達式結(jié)束時不再存在的臨時對象(不在內(nèi)存中占有確定位置的表達式)
便攜方法:對表達式取地址,如果能,則為左值,否則為右值
int val; val = 4; // 正確 ① 4 = val; // 錯誤 ②
上述例子中,由于在之前已經(jīng)對變量val進行了定義,故在棧上會給val分配內(nèi)存地址,運算符=要求等號左邊是可修改的左值,4是臨時參與運算的值,一般在寄存器上暫存,運算結(jié)束后在寄存器上移除該值,故①是對的,②是錯的
左值引用
右值引用
std::move函數(shù)
- std::move作用主要可以將一個左值轉(zhuǎn)換成右值引用,從而可以調(diào)用C++11右值引用的拷貝構(gòu)造函數(shù)
- std::move應(yīng)該是針對你的對象中有在堆上分配內(nèi)存這種情況而設(shè)置的,如下
remove_reference源碼剖析
在分析std::move()
與std::forward()
之前,先看看remove_reference
,下面是remove_reference
的實現(xiàn):
template<typename _Tp> struct remove_reference { typedef _Tp type; }; // 特化版本 template<typename _Tp> struct remove_reference<_Tp&> { typedef _Tp type; }; template<typename _Tp> struct remove_reference<_Tp&&> { typedef _Tp type; };
remove_reference
的作用是去除T
中的引用部分,只獲取其中的類型部分。無論T
是左值還是右值,最后只獲取它的類型部分。
std::forward源碼剖析
轉(zhuǎn)發(fā)左值
template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); }
先通過獲得類型type,定義_t
為左值引用的左值變量,通過static_cast
進行強制轉(zhuǎn)換。_Tp&&
會發(fā)生引用折疊,當(dāng)_Tp
推導(dǎo)為左值引用,則折疊為_Tp& &&
,即_Tp&
,當(dāng)推導(dǎo)為右值引用,則為本身_Tp&&
,即forward
返回值與static_cast
處都為_Tp&&
。
轉(zhuǎn)發(fā)右值
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); }
不同于轉(zhuǎn)發(fā)左值,_t
為右值引用的左值變量,除此之外中間加了一個斷言,表示當(dāng)不是左值的時候,也就是右值,才進行static_cast
轉(zhuǎn)換。
std::move()源碼剖析
// FUNCTION TEMPLATE move template <class _Ty> _NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable return static_cast<remove_reference_t<_Ty>&&>(_Arg); }
std::move的功能是:
- 傳遞的是左值,推導(dǎo)為左值引用,仍舊static_cast轉(zhuǎn)換為右值引用。
- 傳遞的是右值,推導(dǎo)為右值引用,仍舊static_cast轉(zhuǎn)換為右值引用。
- 在返回處,直接范圍右值引用類型即可。還是通過renive_reference獲得_Tp類型,然后直接type&&即可。
所以std::remove_reference<_Tp>::type&&,就是一個右值引用,我們就知道了std::move干的事情了。
小結(jié)
- 在《Effective Modern C++》中建議:對于右值引用使用std::move,對于萬能引用使用std::forward。
- std::move()與std::forward()都僅僅做了類型轉(zhuǎn)換(可理解為static_cast轉(zhuǎn)換)而已。真正的移動操作是在移動構(gòu)造函數(shù)或者移動賦值操作符中發(fā)生的
- 在類型聲明當(dāng)中, “&&” 要不就是一個 rvalue reference ,要不就是一個 universal reference – 一種可以解析為lvalue reference或者rvalue reference的引用。對于某個被推導(dǎo)的類型T,universal references 總是以 T&& 的形式出現(xiàn)。
- 引用折疊是 會讓 universal references (其實就是一個處于引用折疊背景下的rvalue references ) 有時解析為 lvalue references 有時解析為 rvalue references 的根本機制。引用折疊只會在一些特定的可能會產(chǎn)生"引用的引用"場景下生效。這些場景包括模板類型推導(dǎo),auto 類型推導(dǎo), typedef 的形成和使用,以及decltype 表達式。
std::move使用場景
在實際場景中,右值引用和std::move被廣泛用于在STL和自定義類中實現(xiàn)移動語義,避免拷貝,從而提升程序性能。 在沒有右值引用之前,一個簡單的數(shù)組類通常實現(xiàn)如下,有構(gòu)造函數(shù)
、拷貝構(gòu)造函數(shù)
、賦值運算符重載
、析構(gòu)函數(shù)
等。深拷貝/淺拷貝在此不做講解。
class Array { public: Array(int size) : size_(size) { data = new int[size_]; } // 深拷貝構(gòu)造 Array(const Array& temp_array) { size_ = temp_array.size_; data_ = new int[size_]; for (int i = 0; i < size_; i ++) { data_[i] = temp_array.data_[i]; } } // 深拷貝賦值 Array& operator=(const Array& temp_array) { delete[] data_; size_ = temp_array.size_; data_ = new int[size_]; for (int i = 0; i < size_; i ++) { data_[i] = temp_array.data_[i]; } } ~Array() { delete[] data_; } public: int *data_; int size_; };
該類的拷貝構(gòu)造函數(shù)、賦值運算符重載函數(shù)已經(jīng)通過使用左值引用傳參來避免一次多余拷貝了,但是內(nèi)部實現(xiàn)要深拷貝,無法避免。 這時,有人提出一個想法:是不是可以提供一個移動構(gòu)造函數(shù)
,把被拷貝者的數(shù)據(jù)移動過來,被拷貝者后邊就不要了,這樣就可以避免深拷貝了,如:
class Array { public: Array(int size) : size_(size) { data = new int[size_]; } // 深拷貝構(gòu)造 Array(const Array& temp_array) { ... } // 深拷貝賦值 Array& operator=(const Array& temp_array) { ... } // 移動構(gòu)造函數(shù),可以淺拷貝 Array(const Array& temp_array, bool move) { data_ = temp_array.data_; size_ = temp_array.size_; // 為防止temp_array析構(gòu)時delete data,提前置空其data_ temp_array.data_ = nullptr; } ~Array() { delete [] data_; } public: int *data_; int size_; };
這么做有2個問題:
- 不優(yōu)雅,表示移動語義還需要一個額外的參數(shù)(或者其他方式)。
- 無法實現(xiàn)!
temp_array
是個const左值引用,無法被修改,所以temp_array.data_ = nullptr;
這行會編譯不過。當(dāng)然函數(shù)參數(shù)可以改成非const:Array(Array& temp_array, bool move){...}
,這樣也有問題,由于左值引用不能接右值,Array a = Array(Array(), true);
這種調(diào)用方式就沒法用了。
可以發(fā)現(xiàn)左值引用真是用的很不爽,右值引用的出現(xiàn)解決了這個問題,在STL的很多容器中,都實現(xiàn)了以右值引用為參數(shù)的移動構(gòu)造函數(shù)
和移動賦值重載函數(shù)
,或者其他函數(shù),最常見的如std::vector的push_back
和emplace_back
。參數(shù)為左值引用意味著拷貝,為右值引用意味著移動。
class Array { public: ...... // 優(yōu)雅 Array(Array&& temp_array) { data_ = temp_array.data_; size_ = temp_array.size_; // 為防止temp_array析構(gòu)時delete data,提前置空其data_ temp_array.data_ = nullptr; } public: int *data_; int size_; };
如何使用:
// 例1:Array用法 int main(){ Array a; // 做一些操作 ..... // 左值a,用std::move轉(zhuǎn)化為右值 Array b(std::move(a)); }
實例:vector::push_back使用std::move提高性能
// 例2:std::vector和std::string的實際例子 int main() { std::string str1 = "aacasxs"; std::vector<std::string> vec; vec.push_back(str1); // 傳統(tǒng)方法,copy vec.push_back(std::move(str1)); // 調(diào)用移動語義的push_back方法,避免拷貝,str1會失去原有值,變成空字符串 vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1會失去原有值 vec.emplace_back("axcsddcas"); // 當(dāng)然可以直接接右值 } // std::vector方法定義 void push_back (const value_type& val); void push_back (value_type&& val); void emplace_back (Args&&... args);
在vector和string這個場景,加個std::move
會調(diào)用到移動語義函數(shù),避免了深拷貝。
除非設(shè)計不允許移動,STL類大都支持移動語義函數(shù),即可移動的
。 另外,編譯器會默認(rèn)在用戶自定義的class
和struct
中生成移動語義函數(shù),但前提是用戶沒有主動定義該類的拷貝構(gòu)造
等函數(shù)(具體規(guī)則自行百度哈)。 因此,可移動對象在<需要拷貝且被拷貝者之后不再被需要>的場景,建議使用std::move
觸發(fā)移動語義,提升性能。
還有些STL類是move-only
的,比如unique_ptr
,這種類只有移動構(gòu)造函數(shù),因此只能移動(轉(zhuǎn)移內(nèi)部對象所有權(quán),或者叫淺拷貝),不能拷貝(深拷貝)
std::unique_ptr<A> ptr_a = std::make_unique<A>(); std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移動賦值重載函數(shù)‘,參數(shù)是&& ,只能接右值,因此必須用std::move轉(zhuǎn)換類型 std::unique_ptr<A> ptr_b = ptr_a; // 編譯不通過
std::move本身只做類型轉(zhuǎn)換,對性能無影響。 我們可以在自己的類中實現(xiàn)移動語義,避免深拷貝,充分利用右值引用和std::move的語言特性。
std::vector<int> b(5); b[0] = 2; b[1] = 2; b[2] = 2; b[3] = 2; // 此處用move就不會對b中已有元素重新進行拷貝構(gòu)造然后再放到a中 std::vector<int> a = std::move(b);
將vector B賦值給另一個vector A,如果是拷貝賦值,那么顯然要對B中的每一個元素執(zhí)行一個copy操作到A,如果是移動賦值的話,只需要將指向B的指針拷貝到A中即可,試想一下如果vector中有相當(dāng)多的元素,那是不是用move來代替copy就顯得十分高效了呢?建議看一看Scott Meyers 的Effective Modern C++,里面對移動語義、右值引用以及類型推導(dǎo)進行了深入的探索
萬能引用
首先,我們先看一個例子
#include <iostream> using std::cout; using std::endl; template<typename T> void func(T& param) { cout << param << endl; } int main() { int num = 2019; func(num); return 0; }
這樣例子的編譯輸出不存在什么問題,但是如果修改成下面的調(diào)用方式呢?
int main(){ func(2019); return 0; }
編譯器會產(chǎn)生錯誤,因為上面的模板函數(shù)只能接受左值或者左值引用(左值一般是有名字的變量,可以取到地址的),我們當(dāng)然可以重載一個接受右值的模板函數(shù),如下也可以達到效果
template<typename T> void func(T& param) { cout << "傳入的是左值" << endl; } template<typename T> void func(T&& param) { cout << "傳入的是右值" << endl; } int main() { int num = 2019; func(num); func(2019); return 0; }
輸出結(jié)果
傳入的是左值
傳入的是右值
第一次函數(shù)調(diào)用的是左值得版本,第二次函數(shù)調(diào)用的是右值版本。但是,有沒有辦法只寫一個模板函數(shù)即可以接收左值又可以接收右值呢?
C++11中有萬能引用(Universal Reference)的概念:使用T&&
類型的形參既能綁定右值,又能綁定左值
但是注意了:只有發(fā)生類型推導(dǎo)的時候,T&&才表示萬能引用(如模板函數(shù)傳參就會經(jīng)過類型推導(dǎo)的過程);否則,表示右值引用
所以,上面的案例我們可以修改為
template<typename T> void func(T&& param) { cout << param << endl; } int main() { int num = 2019; func(num); func(2019); return 0; }
引用折疊
萬能引用說完了,接著來聊引用折疊(Reference Collapse),因為完美轉(zhuǎn)發(fā)(Perfect Forwarding)的概念涉及引用折疊。一個模板函數(shù),根據(jù)定義的形參和傳入的實參的類型,我們可以有下面四中組合:
左值-左值 T& & # 函數(shù)定義的形參類型是左值引用,傳入的實參是左值引用
template<typename T> void func(T& param) { cout << param << endl; } int main(){ int num = 2021; int& val = num; func(val); }
左值-右值 T& && # 函數(shù)定義的形參類型是左值引用,傳入的實參是右值引用
template<typename T> void func(T& param) { cout << param << endl; } int main(){ int&& val = 2021; func(val); }
右值-左值 T&& & # 函數(shù)定義的形參類型是右值引用,傳入的實參是左值引用
template<typename T> void func(T&& param) { cout << param << endl; } int main(){ int num = 2021; int& val = num; func(val); }
右值-右值 T&& && # 函數(shù)定義的形參類型是右值引用,傳入的實參是右值引用
template<typename T> void func(T&& param) { cout << param << endl; } int main(){ int&& val = 4; func(val); }
但是C++中不允許對引用再進行引用,對于上述情況的處理有如下的規(guī)則:
所有的折疊引用最終都代表一個引用,要么是左值引用,要么是右值引用。規(guī)則是:如果任一引用為左值引用,則結(jié)果為左值引用。否則(即兩個都是右值引用),結(jié)果才是右值引用
即就是前面三種情況代表的都是左值引用,而第四種代表的右值引用
完美轉(zhuǎn)發(fā)
下面接著說完美轉(zhuǎn)發(fā)(Perfect Forwarding),首先,看一個例子
#include <iostream> using std::cout; using std::endl; template<typename T> void func(T& param) { cout << "傳入的是左值" << endl; } template<typename T> void func(T&& param) { cout << "傳入的是右值" << endl; } template<typename T> void warp(T&& param) { func(param); } int main() { int num = 2019; warp(num); warp(2019); return 0; }
輸出的結(jié)果
傳入的是左值
傳入的是左值
是不是和預(yù)期的不一樣,下面我們來分析一下原因:
warp()
函數(shù)本身的形參是一個萬能引用,即可以接受左值又可以接受右值;第一個warp()
函數(shù)調(diào)用實參是左值,所以,warp()
函數(shù)中調(diào)用func()
中傳入的參數(shù)也應(yīng)該是左值;第二個warp()
函數(shù)調(diào)用實參是右值,根據(jù)上面所說的引用折疊規(guī)則,warp()函數(shù)接收的參數(shù)類型是右值引用,那么為什么卻調(diào)用了調(diào)用
func()的左值版本了呢?這是因為在warp()
函數(shù)內(nèi)部,右值引用類型變?yōu)榱俗笾?,因為參?shù)有了名稱,我們也通過變量名取得變量地址
那么問題來了,怎么保持函數(shù)調(diào)用過程中,變量類型的不變呢?這就是我們所謂的“變量轉(zhuǎn)發(fā)”技術(shù),在C++11中通過std::forward()
函數(shù)來實現(xiàn)。我們來修改我們的warp()
函數(shù)如下:
template<typename T> void warp(T&& param) { func(std::forward<T>(param)); }
則可以輸出預(yù)期的結(jié)果
傳入的是左值
傳入的是右值
參考博文
現(xiàn)代C++之萬能引用、完美轉(zhuǎn)發(fā)、引用折疊(萬字長文):https://blog.csdn.net/guangcheng0312q/article/details/103572987
C++ 中的「移動」在內(nèi)存或者寄存器中的操作是什么,為什么就比拷貝賦值性能高呢?:https://www.zhihu.com/question/55735384
一文讀懂C++右值引用和std::move:https://zhuanlan.zhihu.com/p/335994370
到此這篇關(guān)于c++中的std::move函數(shù)的文章就介紹到這了,更多相關(guān)c++ std::move函數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++ 中malloc()和free()函數(shù)的理解
這篇文章主要介紹了C++ 中malloc()和free()函數(shù)的理解的相關(guān)資料,這里提供用法示例幫助大家理解這部分知識,需要的朋友可以參考下2017-08-08關(guān)于C++復(fù)制構(gòu)造函數(shù)的實現(xiàn)講解
今天小編就為大家分享一篇關(guān)于關(guān)于C++復(fù)制構(gòu)造函數(shù)的實現(xiàn)講解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12探討編寫int strlen(char *strDest);不允許定義變量的問題
本篇文章是對編寫int strlen(char *strDest);不允許定義變量的問題進行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05visual studio 2019工具里添加開發(fā)中命令提示符的方法
這篇文章主要介紹了visual studio 2019工具里添加開發(fā)中命令提示符的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03