<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在探討c++11中的Move函數前,先介紹兩個概念(左值和右值)
首先區分左值和右值
左值是表示式結束後依然存在的持久物件(代表一個在記憶體中佔有確定位置的物件)
右值是表示式結束時不再存在的臨時物件(不在記憶體中佔有確定位置的表示式)
便攜方法:對錶示式取地址,如果能,則為左值,否則為右值
int val; val = 4; // 正確 ① 4 = val; // 錯誤 ②
上述例子中,由於在之前已經對變數val進行了定義,故在棧上會給val分配記憶體地址,運運算元=要求等號左邊是可修改的左值,4是臨時參與運算的值,一般在暫存器上暫存,運算結束後在暫存器上移除該值,故①是對的,②是錯的
在分析std::move()
與std::forward()
之前,先看看remove_reference
,下面是remove_reference
的實現:
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
是左值還是右值,最後只獲取它的型別部分。
轉發左值
template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); }
先通過獲得型別type,定義_t
為左值參照的左值變數,通過static_cast
進行強制轉換。_Tp&&
會發生參照摺疊,當_Tp
推導為左值參照,則摺疊為_Tp& &&
,即_Tp&
,當推導為右值參照,則為本身_Tp&&
,即forward
返回值與static_cast
處都為_Tp&&
。
轉發右值
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); }
不同於轉發左值,_t
為右值參照的左值變數,除此之外中間加了一個斷言,表示當不是左值的時候,也就是右值,才進行static_cast
轉換。
// 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的功能是:
所以std::remove_reference<_Tp>::type&&,就是一個右值參照,我們就知道了std::move乾的事情了。
在實際場景中,右值參照和std::move被廣泛用於在STL和自定義類中實現移動語意,避免拷貝,從而提升程式效能。 在沒有右值參照之前,一個簡單的陣列類通常實現如下,有建構函式
、拷貝建構函式
、賦值運運算元過載
、解構函式
等。深拷貝/淺拷貝在此不做講解。
class Array { public: Array(int size) : size_(size) { data = new int[size_]; } // 深拷貝構造 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_; };
該類的拷貝建構函式、賦值運運算元過載函數已經通過使用左值參照傳參來避免一次多餘拷貝了,但是內部實現要深拷貝,無法避免。 這時,有人提出一個想法:是不是可以提供一個移動建構函式
,把被拷貝者的資料移動過來,被拷貝者後邊就不要了,這樣就可以避免深拷貝了,如:
class Array { public: Array(int size) : size_(size) { data = new int[size_]; } // 深拷貝構造 Array(const Array& temp_array) { ... } // 深拷貝賦值 Array& operator=(const Array& temp_array) { ... } // 移動建構函式,可以淺拷貝 Array(const Array& temp_array, bool move) { data_ = temp_array.data_; size_ = temp_array.size_; // 為防止temp_array解構時delete data,提前置空其data_ temp_array.data_ = nullptr; } ~Array() { delete [] data_; } public: int *data_; int size_; };
這麼做有2個問題:
temp_array
是個const左值參照,無法被修改,所以temp_array.data_ = nullptr;
這行會編譯不過。當然函數引數可以改成非const:Array(Array& temp_array, bool move){...}
,這樣也有問題,由於左值參照不能接右值,Array a = Array(Array(), true);
這種呼叫方式就沒法用了。可以發現左值參照真是用的很不爽,右值參照的出現解決了這個問題,在STL的很多容器中,都實現了以右值參照為引數的移動建構函式
和移動賦值過載函數
,或者其他函數,最常見的如std::vector的push_back
和emplace_back
。引數為左值參照意味著拷貝,為右值參照意味著移動。
class Array { public: ...... // 優雅 Array(Array&& temp_array) { data_ = temp_array.data_; size_ = temp_array.size_; // 為防止temp_array解構時delete data,提前置空其data_ temp_array.data_ = nullptr; } public: int *data_; int size_; };
如何使用:
// 例1:Array用法 int main(){ Array a; // 做一些操作 ..... // 左值a,用std::move轉化為右值 Array b(std::move(a)); }
// 例2:std::vector和std::string的實際例子 int main() { std::string str1 = "aacasxs"; std::vector<std::string> vec; vec.push_back(str1); // 傳統方法,copy vec.push_back(std::move(str1)); // 呼叫移動語意的push_back方法,避免拷貝,str1會失去原有值,變成空字串 vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1會失去原有值 vec.emplace_back("axcsddcas"); // 當然可以直接接右值 } // std::vector方法定義 void push_back (const value_type& val); void push_back (value_type&& val); void emplace_back (Args&&... args);
在vector和string這個場景,加個std::move
會呼叫到移動語意函數,避免了深拷貝。
除非設計不允許移動,STL類大都支援移動語意函數,即可移動的
。 另外,編譯器會預設在使用者自定義的class
和struct
中生成移動語意函數,但前提是使用者沒有主動定義該類的拷貝構造
等函數(具體規則自行百度哈)。 因此,可移動物件在<需要拷貝且被拷貝者之後不再被需要>的場景,建議使用std::move
觸發移動語意,提升效能。
還有些STL類是move-only
的,比如unique_ptr
,這種類只有移動建構函式,因此只能移動(轉移內部物件所有權,或者叫淺拷貝),不能拷貝(深拷貝)
std::unique_ptr<A> ptr_a = std::make_unique<A>(); std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移動賦值過載函數‘,引數是&& ,只能接右值,因此必須用std::move轉換型別 std::unique_ptr<A> ptr_b = ptr_a; // 編譯不通過
std::move本身只做型別轉換,對效能無影響。 我們可以在自己的類中實現移動語意,避免深拷貝,充分利用右值參照和std::move的語言特性。
std::vector<int> b(5); b[0] = 2; b[1] = 2; b[2] = 2; b[3] = 2; // 此處用move就不會對b中已有元素重新進行拷貝構造然後再放到a中 std::vector<int> a = std::move(b);
將vector B賦值給另一個vector A,如果是拷貝賦值,那麼顯然要對B中的每一個元素執行一個copy操作到A,如果是移動賦值的話,只需要將指向B的指標拷貝到A中即可,試想一下如果vector中有相當多的元素,那是不是用move來代替copy就顯得十分高效了呢?建議看一看Scott Meyers 的Effective Modern C++,裡面對移動語意、右值參照以及型別推導進行了深入的探索
首先,我們先看一個例子
#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; }
這樣例子的編譯輸出不存在什麼問題,但是如果修改成下面的呼叫方式呢?
int main(){ func(2019); return 0; }
編譯器會產生錯誤,因為上面的模板函數只能接受左值或者左值參照(左值一般是有名字的變數,可以取到地址的),我們當然可以過載一個接受右值的模板函數,如下也可以達到效果
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; }
輸出結果
傳入的是左值
傳入的是右值
第一次函數呼叫的是左值得版本,第二次函數呼叫的是右值版本。但是,有沒有辦法只寫一個模板函數即可以接收左值又可以接收右值呢?
C++11中有萬能參照(Universal Reference)的概念:使用T&&
型別的形參既能繫結右值,又能繫結左值
但是注意了:只有發生型別推導的時候,T&&才表示萬能參照(如模板函數傳參就會經過型別推導的過程);否則,表示右值參照
所以,上面的案例我們可以修改為
template<typename T> void func(T&& param) { cout << param << endl; } int main() { int num = 2019; func(num); func(2019); return 0; }
萬能參照說完了,接著來聊參照摺疊(Reference Collapse),因為完美轉發(Perfect Forwarding)的概念涉及參照摺疊。一個模板函數,根據定義的形參和傳入的實參的型別,我們可以有下面四中組合:
左值-左值 T& & # 函數定義的形參型別是左值參照,傳入的實參是左值參照
template<typename T> void func(T& param) { cout << param << endl; } int main(){ int num = 2021; int& val = num; func(val); }
左值-右值 T& && # 函數定義的形參型別是左值參照,傳入的實參是右值參照
template<typename T> void func(T& param) { cout << param << endl; } int main(){ int&& val = 2021; func(val); }
右值-左值 T&& & # 函數定義的形參型別是右值參照,傳入的實參是左值參照
template<typename T> void func(T&& param) { cout << param << endl; } int main(){ int num = 2021; int& val = num; func(val); }
右值-右值 T&& && # 函數定義的形參型別是右值參照,傳入的實參是右值參照
template<typename T> void func(T&& param) { cout << param << endl; } int main(){ int&& val = 4; func(val); }
但是C++中不允許對參照再進行參照,對於上述情況的處理有如下的規則:
所有的摺疊參照最終都代表一個參照,要麼是左值參照,要麼是右值參照。規則是:如果任一參照為左值參照,則結果為左值參照。否則(即兩個都是右值參照),結果才是右值參照
即就是前面三種情況代表的都是左值參照,而第四種代表的右值參照
下面接著說完美轉發(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; }
輸出的結果
傳入的是左值
傳入的是左值
是不是和預期的不一樣,下面我們來分析一下原因:
warp()
函數本身的形參是一個萬能參照,即可以接受左值又可以接受右值;第一個warp()
函數呼叫實參是左值,所以,warp()
函數中呼叫func()
中傳入的引數也應該是左值;第二個warp()
函數呼叫實參是右值,根據上面所說的參照摺疊規則,warp()函數接收的引數型別是右值參照,那麼為什麼卻呼叫了呼叫
func()的左值版本了呢?這是因為在warp()
函數內部,右值參照型別變為了左值,因為引數有了名稱,我們也通過變數名取得變數地址
那麼問題來了,怎麼保持函數呼叫過程中,變數型別的不變呢?這就是我們所謂的“變數轉發”技術,在C++11中通過std::forward()
函數來實現。我們來修改我們的warp()
函數如下:
template<typename T> void warp(T&& param) { func(std::forward<T>(param)); }
則可以輸出預期的結果
傳入的是左值
傳入的是右值
參考博文
現代C++之萬能參照、完美轉發、參照摺疊(萬字長文):https://blog.csdn.net/guangcheng0312q/article/details/103572987
C++ 中的「移動」在記憶體或者暫存器中的操作是什麼,為什麼就比拷貝賦值效能高呢?:https://www.zhihu.com/question/55735384
一文讀懂C++右值參照和std::move:https://zhuanlan.zhihu.com/p/335994370
到此這篇關於c++中的std::move函數的文章就介紹到這了,更多相關c++ std::move函數內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45