std::move

一、move的概念

1、在学习move之前需要知道左值、右值、左值引用、右值引用的概念,见:https://www.cnblogs.com/judes/p/15159463.html

学习之后需要知道一个重点

移动构造不进行深拷贝,直接使用右值的资源。【move是用来服务于此重点的】

2、概念

move将一个左值强制转化为右值,继而可以通过右值引用使用该值。

原型:

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

其中:remove_reference<T>::type是去除T自身可能携带的引用。

注意1:move是一个常量表达式函数,没有做啥高级的操作,单纯的是将一个左值强制转化为右值;

注意2:右值其实就是之前“临时变量”的概念;

注意3:通过move一个变量a,只是返回一个右值,这个右值是根据a转换来的,执行move(a)后,对变量a没有任何改变,这是关键所在,也是让人迷糊的地方;

注意4:高级的地方是在需要使用到复制对象【类的对象,如果是单纯的基本数据如int、double则无用处,因为这些本身就是右值,也不存在构造】的时候,move用来配合生成右值,右值可用于调用移动构造函数;

二、帮助理解

甲有一本电子书,乙需要这本书,有多个方法实现这个场景。

①、甲将书拍照,而且只拍目录给乙,乙只能看个大概【对应浅拷贝】;

②、甲将书每一页都手抄下来,抄完之后给乙【对应深拷贝,深拷贝构造函数都是const引用,但这里实际上应该是乙来抄,因为是在乙的构造函数里深拷贝】;

③、甲将电子书复制一个副本【右值】发给乙,乙的任何操作不会影响甲的电子书【对应移动构造,输入右值】

综上我们引入move的目的流程就是:

我们需要右值【因为最后一步的需求】--------->move提供左值转右值强制转换功能--------->用于服务移动构造函数--------->移动构造里里直接将右值对象的资源给本对象。

三、矛盾点

如果使用普通浅拷贝,那本对象只能与原来对象共用资源;

如果使用深拷贝,那本对象会复制原对象所有资源,会消耗性能;

解决此矛盾点:

通过原对象生成一份右值,通过此右值来初始化本对象,此本对象不需要深拷贝,浅拷贝即可,并且不影响原对象资源。而这个右值初始化对象操作我们一般放在移动构造里实现。

四、使用

1、用右值初始化对象

①、使用普通构造

#include <iostream>
#include <vector>

class B
{
public:
    B() { }
    B(const B&) {  }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    {
        std::cout << "A Copy Constructor, new something" << std::endl;
    }
    A(A&& src) noexcept :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor, don't new anything" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    return a;
}

int main()
{
    A a = getA();
    A a1(a);
    //A a1(std::move(a));
    return 0;
}

调用了拷贝构造函数,实现了深拷贝,耗费了性能,打印:

 ②、使用移动构造

#include <iostream>
#include <vector>

class B
{
public:
    B() { }
    B(const B&) {  }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    {
        std::cout << "A Copy Constructor, new something" << std::endl;
    }
    A(A&& src) noexcept :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor, don't new anything" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    return a;
}

int main()
{
    A a = getA();
    //A a1(a);
    A a1(std::move(a));
    return 0;
}

调用了移动构造函数,只需要浅拷贝,打印:

 2、高效交换值

template <class T>
void MoveSwap(T & a, T & b)
{
    T tmp = move(a);  //std::move(a) 为右值,这里会调用移动构造函数
    a = move(b);  //move(b) 为右值,因此这里会调用移动赋值运算符
    b = move(tmp);  //move(tmp) 为右值,因此这里会调用移动赋值运算符
}
 
template <class T>
void Swap(T & a, T & b) 
{
    T tmp = a;  //调用复制构造函数
    a = b;  //调用复制赋值运算符
    b = tmp;  //调用复制赋值运算符
}

五、实际的作用【纯个人理解】

①、对于单个的或指定数量的对象复制处理中,不需要引入这种晦涩难懂的玩意儿,好处是可以理解左值、右值,也就是以前的临时变量概念【现在这个概念被淡化了,右值概念来替代】,减少临时变量的生成,写多了反而让队友看不懂。【本人学习move这概念花了2周左右,因为其牵扯了左右值、移动构造、深浅拷贝等概念,实际上的move就是个常量表达式】

②、批量的复制,如自定义一个类,这个类存储的是我们需要处理任务的最小单元,也就是生产者消费者模式里的单个任务,里面的属性可能有指针,指针涉及到初始化。当把多个这样的类对象放在类似于vector这样的连续性容器里时,一旦在倒数n处的位置进行push或insert时,此位置之后的所有对象都得往后移动一位,即产生nx1次拷贝,时间复杂度是O(n);针对这种情况,我们就可以为类设计一个移动构造函数,并实现资源的重新指向,如此之后在进行push时,我们每次都是用前面一个对象的右值来初始化下一个位置的对象,时间复杂度为O(1)【考虑首尾的话就是O(2)?】,效果不言而喻。

PS:

1、右值引用可以用来改变右值,它是一个左值

class A{
   ..... 
};
A a;
A b = std::move(a);//这里并没有看到右值引用,单纯的用右值来调用移动构造初始化b,移动构造的入参就是右值引用

std::move(a)是生成一个右值,继而可以用右值引用,右值转化为右值引用是通过函数入参或直接声明来实现的:

方式1:
A&& rf = std::move(a);

方式2:
void fun(A&& rf)
{
    rf.xxxx = nullptr;
}

其中方式2可以是普通函数,但更常见在于移动构造函数、移动赋值函数【我更喜欢统称移动构造函数】

所谓的改变右值,一般就是将这个右值里指针【一般有指针的时候才会触发这种情景】置为空,这就达到了将右值的资源全转为本对象

2、move不改变原始值,但一般跟原始值即将不存在的情况使用

如函数返回值初始化对象、vector的push操作、两值交换等




长风破浪会有时,直挂云帆济沧海!
可通过下方链接找到博主
https://www.cnblogs.com/judes/p/10875138.html
原文地址:https://www.cnblogs.com/judes/p/15159454.html