第14課 移動語義

一. 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.  //調用移動構造函數
*/
相關文章
相關標籤/搜索