一. std::moveios
(一)std::move的原型編程
template<typename T> decltype(auto) move(T&& param) //注意,形參是個引用(萬能引用) { using ReturnType = typename remove_reference<T>::type&&; //去除T自身可能攜帶的引用 return static_cast<ReturnType>(param); //強制轉換爲右值引用類型 }
(二)注意事項安全
1. std::move的本質就強制類型轉換,它無條件地將實參轉爲右值引用類型(匿名對象,是個右值),繼而用於移動語義。函數
2. 該函數只是將實參轉爲右值,除此以外並無真正的move任何東西。實際上,它在運行期沒任何做爲,編譯器也不會爲它生成任何的可執行代碼,連一個字節都沒有。性能
3. 若是要對某個對象執行移動操做時,則不要將其聲明爲常量。由於針對常量對象執行移動操做將變成複製操做。測試
二. 移動語義優化
(一)深拷貝和移動的區別this
1. 深拷貝:將SrcObj對象拷貝到DestObj對象,須要同時將Resourse資源也拷貝到DestObj對象去。這涉及到內存的拷貝。spa
2. 移動:經過「偷」內存的方式,將資源的全部權從一個對象轉移到另外一個對象上。但只是轉移,並無內存的拷貝。可見Resource的全部權只是從SrcObj對象轉移到DestObj對象,因爲不存在內存拷貝,其效率通常要高於複製構造。指針
(二)複製和移動操做函數
1. 複製/移動操做的函數聲明
①Object(T&); //複製構造,僅接受左值 ②Object(const T&); //複製構造,便可以接受左值又可接收右值 ③Object(T&&) noexcept; //移動構造,僅接受右值 ④T& operator=(const T&);//複製賦值函數,便可以接受左值又可接收右值 ⑤T& operator=(T&&); //移動賦值函數,僅接受右值
2. 注意事項
①移動語義必定是要修改臨時對象的值,因此聲明移動構造時應該形如Test(Test&&),而不能聲明爲Test(const Test&&)
②默認的移動構造函數實際上跟默認的拷貝構造函數同樣,都是「淺拷貝」。一般狀況下,必須自定義移動構造函數。
③對於移動構造函數來講,拋出異常是很危險的。由於移動語義還沒完成,一個異常就拋出來,可能會形成懸掛指針。所以,應儘可能經過noexcept聲明不拋出異常,而一旦出現異常就能夠直接調用std::terminate終止程序。
④特殊成員函數之間存在相互抑制的生成機制,可能會影響到默認拷貝構造和默認移動構造函數的自動生成。(詳見《特殊成員函數的生成機制》一節)
【編程實驗】move移動語義
#include <iostream> #include <vector> using namespace std; //1. 移動語義 class HugeMem { public: int* buff; int size; HugeMem(int size) : size(size > 0 ? size : 1) { buff = new int[size]; } //移動構造函數 HugeMem(HugeMem&& hm) noexcept : size(hm.size), buff(hm.buff) { hm.buff = nullptr; } ~HugeMem() { delete[] buff; } }; class Moveable { public: HugeMem h; int* i; public: Moveable() : i(new int(3)), h(1024){} //移動構造函數(強制轉爲右值,以調用h的移動構造函數。注意m雖然是右值 //引用,但形參是具名變量,m是個左值。所以m.h也是左值,需轉爲右值。 Moveable(Moveable&& m) noexcept: i(m.i), h(std::move(m).h) { m.i = nullptr; } ~Moveable() { delete i; } }; Moveable GetTemp() { Moveable tmp = Moveable(); cout << hex << "Huge mem from " << __func__ << " @" << tmp.h.buff << endl; return tmp; } //2. 對常量對象實施移動將變成複製操做 class Annotation { std::string value; public: //注意:對常量的text對象實施移動操做時,因爲std::move(text)返回的結果是個 //const std::string對象,因爲帶const,不能匹配string(&& rhs)移動構造函數, //但匹配string(const string& rhs)複製構造函數,所以當執行value(std::move(text)) //時,其實是將text複製給value。對於非string類型的狀況也同樣,所以對常量對象的 //移動操做實際上會變成複製操做! explicit Annotation(const std::string text) : value(std::move(text)) { } }; //3. 利用移動語義實現高性能的swap函數 template<typename T> void Swap(T& a, T& b) noexcept //聲明爲noexcept以便在交換失敗時,終止程序 { //若是a、b是可移動的,則直接轉移資源的全部權 //若是是不可移動的,則經過複製來交換兩個對象。 T tmp(std::move(a)); //先把a的資源轉交給tmp a = std::move(b); b = std::move(tmp); } int main() { //1. 移動語義 Moveable a(GetTemp()); //移動構造 cout << hex << "Huge mem from " << __func__ << " @" << a.h.buff << endl; return 0; } /*輸出結果 Huge mem from GetTemp @02C66248 (從中能夠看出Huge mem從臨時對象移動了a對象) Huge mem from main @02C66248 */
3、正確理解移動語義
(一) 「移動」操做其實是一種請求,由於有些類型不存在移動操做,對於這些對象會經過其複製操做來實現「移動」。
(二)某些類型的移動操做未必比複製操做更快。如:
1. std::vector和std::array。
(1)標準庫大部分容器類(如vector),內部是將其元素Widgets存放在堆上,而後用指針指向該堆內存。在進行移動操做時,只是進行指針的複製。整個容器內容在常數時間內即可移動完成。
(2)而std::array對象缺乏這樣的一根指針,由於其內容數據是直接存儲對象上的。雖然std::array提供移動操做,但其移動和複製的速度哪一個更快,取決於元素Widget的移動和複製速度的比較。同時std::array移動時須要對每個元素進行移動,老是須要線性時間。
2. 許多std::string類型的實現採用了小型字符串優化(SSO)。當使用SSO後,「小型」字符串(如不超過15個字符)會存儲在std::string對象內的某個緩衝區內,即內容直接存儲在對象上(而不是堆上)。所以,此時是整個對象的移動,速度並比複製更快。
(三)標準庫一些容器操做提供了強異常安全保證,爲了兼容C++98的遺留代碼在升級到C++11時仍保證正確性。庫中用std::move_if_noexcept模板來替代move函數。該函數在類的移動構造函數沒有聲明noxcept關鍵字時返回一個左值引用從而使變量經過拷貝語義,而在移動構造函數有noexcept時返回一個右值引用,從而使變量可使用移動語義。移動操做未加noexcept時,編譯器仍會強制調用一個複製操做。
【編程實驗】正確理解移動語義
#include <iostream> #include <chrono> #include <vector> #include <array> #include <thread> using namespace std; //1. 移動不存在時,實行的是複製操做 class Foo { public: Foo(){} Foo(const Foo&) { cout <<"Foo(const Foo&)" << endl; } }; //2. 移動速度未必比複製快 //2.1 輔助類(元素類) class Widget { public: Widget() = default; Widget(const Widget&) { //模擬複製操做,假設須要1毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(1)); } Widget(Widget&&) { //模擬移動操做,假設須要2毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(2)); } Widget& operator=(const Widget&) { //模擬複製賦值操做,假設須要1毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(1)); return *this; } Widget& operator=(Widget&&) { //模擬移動賦值操做,假設須要2毫秒 std::this_thread::sleep_for(std::chrono::milliseconds(2)); return *this; } }; //2.2. 計算任意函數的執行時間:auto&&用於lambda表達式形參(C++14) auto funcTimer = [](auto&& func, auto&& ... params) { //計時器啓動 std::chrono::system_clock::time_point t1 = std::chrono::system_clock::now(); //調用func(param...)函數 std::forward<decltype(func)>(func)( //根據func的左右值特性來調用相應的重載&或&&版本的成員函數 std::forward<decltype(params)>(params)... //保持參數的左/右值特性 ); std::chrono::system_clock::time_point t2 = std::chrono::system_clock::now(); long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count(); cout << elapsed << " microseconds" << endl; }; //2.3 複製和移動操做 auto lamMove = [](auto&& src) { auto dest = std::move(src); return; }; auto lamCopy = [](auto&& src) { auto dest = src; return; }; //2.4 測試vector類 void testVector() { std::vector<Widget> vw1{ 10,Widget() }; cout <<"copy vector: " ; funcTimer(lamCopy, vw1); //測試移動操做用時 cout << "move vector: "; funcTimer(lamMove, vw1); } //2.5 測試array類 void testArray() { std::array<Widget, 10> aw1; cout << "copy array: "; funcTimer(lamCopy, aw1); //測試移動操做用時 cout << "move array: "; funcTimer(lamMove, aw1); } //3. move_if_noexcept的用法 struct Maythrow { Maythrow() {} Maythrow(const Maythrow&) { cout <<"Maythrow copy construct." << endl; } Maythrow(Maythrow&&) { cout << "Maythrow move construct." << endl; } }; struct Nothrow { Nothrow() {} Nothrow(const Nothrow&) { cout << "Nothrow copy construct." << endl; } Nothrow(Nothrow&&) noexcept { //注意,這裏聲明爲noexcept! cout << "Nothrow move construct." << endl; } }; int main() { //1. 移動操做不存在時 Foo f1; Foo f2 = std::move(f1); //調用複製構造函數 //2. 移動速度未必比複製快 testVector(); testArray(); //3. 移動未聲明爲noexcept時,調用複製構造 Maythrow m; Nothrow n; Maythrow mt = move_if_noexcept(m); //move_if_noexcept返回左值引用,調用複製構造函數 Nothrow nt = move_if_noexcept(n); //move_if_noexcept返回右值引用,調用移動構造函數 return 0; } /*輸出結果 Foo(const Foo&) copy vector: 19825 microseconds move vector: 5 microseconds //常量時間 copy array: 19109 microseconds move array: 29589 microseconds //移動的速度未必比複製快!取決於Widget的移動和複製速度的比較! Maythrow copy construct. //調用複製構造函數 Nothrow move construct. //調用移動構造函數 */