1. 程式人生 > >第15課 右值引用(2)_std::move和移動語義

第15課 右值引用(2)_std::move和移動語義

可見 div 強制轉換 let 技術分享 移動語義 ptr align 講解

1. std::move

(1)std::move的原型

template<typename T>
typename remove_reference<T>::type&& 
move(T&& param)
{
    using ReturnType = remove_reference<T>::type&&
    
    return static_cast<ReturnType>(param);
}

(2)std::move的作用

  ①std::move函數的本質就是強制轉換它無條件地將參數轉換為把一個右值引用

,又由於函數返回的右值引用(匿名對象)是一個右值。因此,std::move所做的所有事情就是轉換它的參數為一個右值。繼而用於移動語義。

  ②該函數只是轉換它的參數為右值,除此之外並沒有真正的move任何東西。Std::move應用在對象上能告訴編譯器,這個對象是有資格被move的。這也是為什麽std::move有這樣的名字:能讓指定的對象更容易被move。

2. 移動語義

(1)深拷貝和移動的區別

技術分享

  ①深拷貝:將SrcObj對象拷貝到DestObj對象,需要同時將Resourse資源也拷貝到DestObj對象去。這涉及到內存的拷貝。

  ②移動:是將資源的所有權從一個對象轉移到另一個對象上

但只是轉移,並沒有內存的拷貝。可見Resource的所有權只是從SrcObj對象轉移到DestObj對象,無須拷貝。

【編程實驗】拷貝和移動語義

#include <iostream>

using namespace std;

//編譯選項:g++ -std=c++11 test0.cpp -fno-elide-constructors

class Test
{
public:
   int* m;  //所涉及的內存資源
   
   //用於統計構造函數、析構函數和拷貝構造等函數被調用的次數
   static int n_ctor; 
   static int n_dtor;
   
static int n_cptor; static int n_mctor; public: Test() : m(new int(0)) { cout <<"Construct: "<< ++n_ctor << endl; } Test(const Test& t) : m(new int(*t.m)) { cout <<"Copy Construct: "<< ++n_cptor << endl; } //移動構造函數 /* Test(Test&& t) : m(t.m) { t.m = nullptr; cout <<"move Construct: "<< ++n_mctor << endl; } */ ~Test() { cout <<"Destruct: " << ++n_dtor <<endl; } }; int Test::n_ctor = 0; int Test::n_dtor = 0; int Test::n_cptor = 0; int Test::n_mctor = 0; Test getTemp() { Test t; cout <<"Resource from: " << __func__ << ": " << hex << t.m << endl; return t; //編譯器會嘗試以下幾種方式進行優化。(見後面的RVO優化部分的講解) //1. 優選被NRVO優化。 //2. std::move(Test()),再調用move constructors //3. 如果以上兩者均失敗,則調用copy constructors; } //高性能的Swap函數 template<typename T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); } int main() { /*實驗時,可以通過註釋和取消註釋移動構造函數兩種方式來對比*/ Test t = getTemp(); cout <<"Resource from: " << __func__ << ": " << hex << t.m << endl; return 0; } /*實驗結果: 1. 不存在移動構造函數時 Construct: 1 Resource from: getTemp: 0x3377f0 move Construct: 1 Destruct: 1 move Construct: 2 Destruct: 2 Resource from: main: 0x3377f0 Destruct: 3 2. 存在移動構造函數時 Construct: 1 Resource from: getTemp: 0x5477f0 Copy Construct: 1 Destruct: 1 Copy Construct: 2 Destruct: 2 Resource from: main: 0x547810 Destruct: 3 以上實驗結果表明,雖然調用拷貝構造或移動構造的次數沒有減少,但由於 拷貝構造涉及內存的拷貝,而移動構造只是資源的轉移效率會更高。 */

(2)移動語義

  ①臨時對象的產生和銷毀以及拷貝的發生對於程序員來說基本上是透明的,不會影響程序的正確性,但可能影響程序的執行效率且不易被察覺到。

  ②移動語義則是通過“偷”內存的方式,將資源從一個對象轉移到另一個對象身上,由於不存在內存拷貝,其效率一般要高於拷貝構造

【編程實驗】std::move與移動語義

#include <iostream>
using namespace std;

//編譯選項:g++ -std=c++11 test2.cpp -fno-elide-constructors

//資源類(假設該類占用內存資源較多)
class HugMem
{
public:
    HugMem(){cout <<"HugMem()" << endl;}
    HugMem(const HugMem&){cout <<"HugMem(const HugMem&)" << endl; }
    ~HugMem(){cout <<"~HugMem()" << endl;};
};

class Test
{
private:
   HugMem* hm;
public:
    Test()
    {
        hm = new HugMem();
        cout << "Test Constructor" << endl;
    }
    
    Test(const Test& obj): hm (new HugMem(*obj.hm))
    {
        cout << "Test Copy Constructor" << endl;
    }
    
    //移動構造函數
    Test(Test&& obj) : hm(obj.hm) //資源轉移
    {
        obj.hm = nullptr; //讓出資源的所有權
        cout << "Test Move Constructor" << endl;
    }
    
    Test& operator=(const Test& obj)
    {
        if(this != &obj){
            hm = new HugMem(*obj.hm);
            cout << "operator=(const Test& obj)" << endl;
        }
        return *this;
    }
    
    Test& operator=(Test&& obj)
    {
        if(this != &obj){
            hm = obj.hm;
            obj.hm = nullptr;
            cout << "operator=(const Test&& obj)" << endl;
        }
        return *this;
    }
    
    ~Test()
    {
        delete hm;
        //cout <<"~Test()" << endl;
    }
};

Test getTest()
{
    Test tmp; 
    
    return tmp; //這裏可能會被編譯器優化,見返回值優化部分
}

int main()
{
    Test t1;
    
    cout << "===============================================" << endl;
    
    Test t2(t1); //拷貝構造
    
    cout << "===============================================" << endl;
    Test t3(std::move(t2)); //移動構造
    
    cout << "===============================================" << endl;
    t3 = getTest();//移動賦值
    
    t1 = t3;    //拷貝賦值
    
    cout << "===============================================" << endl;
    Test t4 = getTest();  //從臨時對象->t4,調用移動構造,然後臨時對象銷毀
    cout << "===============================================" << endl;
    Test&& t5 = getTest(); //t5直接將臨時對象接管過來,延長了其生命期
                           //註意與t4的區別

    return 0;
}
/*輸出結果:
e:\Study\C++11\15>g++ -std=c++11 test2.cpp -fno-elide-constructors
e:\Study\C++11\15>a.exe
HugMem()
Test Constructor
===============================================
HugMem(const HugMem&)
Test Copy Constructor
===============================================
Test Move Constructor
===============================================
HugMem()
Test Constructor
Test Move Constructor
operator=(const Test&& obj)
HugMem(const HugMem&)
operator=(const Test& obj)
===============================================
HugMem()
Test Constructor
Test Move Constructor
Test Move Constructor
===============================================
HugMem()
Test Constructor
Test Move Constructor
~HugMem()
~HugMem()
~HugMem()
~HugMem()
*/

(3)其它問題

  ①移動語義一定是要修改臨時對象的值,所以聲明移動構造時應該形如Test(Test&&),而不能聲明為Test(const Test&&)

  ②默認情況下,編譯器會隱式生成一個移動構造函數,而如果自定義了拷貝構造函數、拷貝賦值函數、移動賦值函數、析構函數中的任何一個或多個,編譯器就不會再提供默認的默認的版本。

  ③默認的移動構造函數實際上跟默認的拷貝構造函數是一樣的,都是淺拷貝。通常情況下,如果需要移動語義,必須自定義移動構造函數。

  ④在移動構造函數是拋出異常是危險的。因為異常拋出時,可能移動語義還沒完成,這會導致一些指針成為懸掛指針。可以為其添加一個noexcept關鍵字以保證移動構造函數中拋出來的異常會直接調用terminate來終止程序。

3. RVO/NRVO和std::move

(1)RVO/NRVO返回值優化:是編譯器的一項優化技術,它的功能主要是消除為保存函數返回值而創建的臨時對象

class X
{
public:
    X(){cout <<"constructor..." << endl;}
    X(const X& x){cout <<"copy constructor" << endl;}
};

//1、按值返回匿名的臨時對象(RVO)
X func()
{
    return X(); //RVO優化
}


//2、按值返回具名局部對象(NRVO)
X func()
{
    X x;
    
    //返回方式1:
    return x; //按值返回具名對象:NRVO優化(編譯器自動優化,效率高!)
    
    //返回方式2:(不要這樣做!//return std::move(x);//x轉為右值,本意是通過調用move constructor來避開move constructor來提高效率。但實際上,
                          //效率比return x更低,因為後者會被編譯器默認地采用更高效的NRVO來優化。    
}

//NRVO偽代碼:
void func(X& result) //註意多了一個參數,修改了函數原型
{
    //編譯器所產生的default constructor的調用
    result.X::X(); //C++偽代碼,調用X::X()
    
    return;
}
           
X xs = func();

(2)std::move與RVO的關系

  ①編譯器會盡可能使用RVO和NRVO來進行優化。由於std::move(localObj)的返回值類型帶有引用修飾符,反而不滿足標準中的RVO條件,這樣編譯器只能選擇move constructor。

  ②當無法使用RVO優化時,由於按值返回的是個右值,編譯器會隱式調用std::move(localobj)來轉換,從而盡力調用move constructor

  ③如果以上這些都失敗了,編譯器才會調用copy constructor

第15課 右值引用(2)_std::move和移動語義