• C++——右值引用、移动构造函数、move函数、完美转发


    左值引用vs右值引用

    左值引用:&

    int num = 10;
    int &b = num; //num为左值 正确
    int &c = 10; //错误,左值引用不能初始化为右值
    
    • 1
    • 2
    • 3

    右值引用:&&
    和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

    int num = 10;
    int && a = num;  //错误,右值引用不能初始化为左值
    int && a = 10;   //正确
    
    • 1
    • 2
    • 3

    移动构造函数

    在 C++ 11 标准之前,如果想用其它对象初始化一个同类的新对象,只能借助类中的拷贝构造函数。当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝的方式复制该指针成员。

    看下面的例子:

    #include <iostream>
    using namespace std;
    class demo{
    public:
       demo():num(new int(0)){
          cout<<"construct!"<<endl;
       }
       //拷贝构造函数
       demo(const demo &d):num(new int(*d.num)){
          cout<<"copy construct!"<<endl;
       }
       ~demo(){
          cout<<"class destruct!"<<endl;
       }
    private:
       int *num;
    };
    demo get_demo(){
        return demo();
    }
    int main(){
        demo a = get_demo();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    如上所示,我们为 demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。

    执行结果如下:

    construct!                <-- 执行 demo()
    copy construct!       <-- 执行 return demo()
    class destruct!         <-- 销毁 demo() 产生的匿名对象
    copy construct!       <-- 执行 a = get_demo()
    class destruct!         <-- 销毁 get_demo() 返回的临时对象
    class destruct!         <-- 销毁 a
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    由结果可以得出,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次深拷贝操作。

    怎样才能避免深拷贝导致的效率问题呢?——移动语义

    什么是移动语义?
    所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。而移动构造函数就是现实移动语义的具体方式

    以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。

    下面程序对 demo 类进行了修改:

    #include <iostream>
    using namespace std;
    class demo{
    public:
        demo():num(new int(0)){
            cout<<"construct!"<<endl;
        }
        demo(const demo &d):num(new int(*d.num)){
            cout<<"copy construct!"<<endl;
        }
        //借助右值引用添加移动构造函数
        demo(demo &&d):num(d.num){
            d.num = NULL;
            cout<<"move construct!"<<endl;
        }
        ~demo(){
            cout<<"class destruct!"<<endl;
        }
    private:
        int *num;
    };
    demo get_demo(){
        return demo();
    }
    int main(){
        demo a = get_demo();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

    输出结果为:

    construct!
    move construct!
    class destruct!
    move construct!
    class destruct!
    class destruct!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

    临时对象既无名称也无法获取其存储地址,所以属于右值,可以初始化右值引用

    结论:
    当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数

    我们在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

    move函数

    上面讲到,当使用类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数
    那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。

    move 的功能很简单,就是将某个左值强制转化为右值。

    #include <iostream>
    using namespace std;
    class movedemo{
    public:
        movedemo():num(new int(0)){
            cout<<"construct!"<<endl;
        }
        //拷贝构造函数
        movedemo(const movedemo &d):num(new int(*d.num)){
            cout<<"copy construct!"<<endl;
        }
        //移动构造函数
        movedemo(movedemo &&d):num(d.num){
            d.num = NULL;
            cout<<"move construct!"<<endl;
        }
    public:     //这里应该是 private,使用 public 是为了更方便说明问题
        int *num;
    };
    int main(){
        movedemo demo;
        cout << "demo2:\n";
        movedemo demo2 = demo;
        //cout << *demo2.num << endl;   //可以执行
        cout << "demo3:\n";
        movedemo demo3 = std::move(demo);
        //此时 demo.num = NULL,因此下面代码会报运行时错误
        //cout << *demo.num << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    程序执行结果为:

    construct!
    demo2:
    copy construct!
    demo3:
    move construct!
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过观察程序的输出结果,以及对比 demo2 和 demo3 初始化操作不难得知,demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。

    完美转发

    什么是完美转发?
    它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

    举个不完美转发的例子:

    template<typename T>
    void function(T t) {
        otherdef(t);
    }
    
    • 1
    • 2
    • 3
    • 4

    显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。

    怎么才能让上面的function() 函数模板实现完美转发呢?
    C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
    借助右值引用,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:

    template <typename T>
    void function(T&& t) { //参数 t 既可以接收左值,也可以接收右值。
        otherdef(t);
    }
    
    • 1
    • 2
    • 3
    • 4

    通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。
    但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?

    C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword(),我们只需要调用该函数,就可以很方便地解决此问题。

    实现完美转发的函数模板:

    template <typename T>
    void function(T&& t) {
        otherdef(forward<T>(t));
    }
    
    • 1
    • 2
    • 3
    • 4

    此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。

  • 相关阅读:
    Midjourney v6.5 可能会在“7月底”发布,并改进了真实感和皮肤纹理
    Wireshark基本使用方法
    【操作系统】1.3.1 操作系统的运行机制
    C++中 system(pause);的用法与意义
    mysql大表的更新和删除
    语音转文字怎么转?分享这些实用软件
    《昇思25天学习打卡营第25天 | 昇思MindSporeResNet50迁移学习》
    [Games101] Lecture 05 Rasterization 1 (Triangles)
    滴滴 Redis 异地多活的演进历程
    字节跳动工程师收入世界第五,2021年全球程序员收入报告出炉
  • 原文地址:https://blog.csdn.net/qq_40337086/article/details/125458782