• 第14课 移动语义(std::move)


    一. std::move

    (一)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. 如果要对某个对象执行移动操作时,则不要将其声明为常量。因为针对常量对象执行移动操作将变成复制操作

    二. 移动语义

    (一)深拷贝和移动的区别

     

      1. 深拷贝:将SrcObj对象拷贝到DestObj对象,需要同时将Resourse资源也拷贝到DestObj对象去。这涉及到内存的拷贝。

      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
    */

    三、正确理解移动语义

    (一) “移动”操作实际上是一种请求,因为有些类型不存在移动操作,对于这些对象会通过其复制操作来实现“移动”。

    (二)某些类型的移动操作未必比复制操作更快。如:

      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.  //调用移动构造函数
    */
  • 相关阅读:
    unix文件权限
    jira部署,主机迁移,数据库迁移,jira
    c函数习记
    常用软介质下载
    Matlab interpgui
    LightOJ 1422
    【CODEFORCES】 A. Keyboard
    leetcode 230: Kth Smallest Element in a BST
    Vertica7 Native Connection Load Balance
    vlc模块间共享变量
  • 原文地址:https://www.cnblogs.com/5iedu/p/11318729.html
Copyright © 2020-2023  润新知