第15课 右值引用_std::move和移动语义

Posted 浅墨浓香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第15课 右值引用_std::move和移动语义相关的知识,希望对你有一定的参考价值。

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课 右值引用_std::move和移动语义的主要内容,如果未能解决你的问题,请参考以下文章

强制转换为右值

静态转换为右值引用和 std::move 之间有啥区别吗

第14课 右值引用_基本概念

C++ 11 右值引用以及std::move

关于右值和移动构造

std::forward vs std::move 同时将左值绑定到右值引用