Cpp中move的原理


概述

怎么用就不多说了,也不扯语言学家的问题,主要来研究一下实现原理是什么?

移动不移动

正如完美转发不完美。

右值引用本身其实就是一个栈上的普通变量,它是 lvalue。而使用右值引用做函数参数是为了优先匹配直接传入函数返回值的情况,从而跟常引用做参数来进行区分。通常情况下,用右值引用接收一个对象,是为了复用它的资源,来进行浅复制的。就好像,我们把原本的资源「移动」到了新的对象当中去,因此称之为「移动语义」。含有移动语义的构造函数就称为「移动构造函数」、含有移动语义的赋值函数就称为「移动赋值函数」。所以大家一定要清楚,这里的「移动」是「语义」上的,并没有真的移动,一般就是用来做浅复制的。

std::move就是帮助我们做一件事情,即在构造一个变量(或者其他情况)的时候不使用拷贝构造而是使用移动构造。我们已经知道,如果想进行移动语义,那么就需要用右值引用来接收。但目前我们要操作的值是一个左值(不然也就不存在需要移动的情况),那么如何让编译器知道我们要进行的操作?让它当作右值来处理,因为就像上文说的,移动不移动,又不会真的去 check 入参的左右性。

以下面的代码为例:

void Demo() {
  String str1;

  String str2 = str1; // 这里会调用拷贝构造,因为str1是左值
}

要把这个 str1,伪装成右值,骗过编译器去触发右值引用的重载函数。

void Demo() {
  String str1;

  String str2 = static_cast<String &&>(str1); // 强制转成右值引用,去触发移动构造函数
}

这里的=号不是赋值的等号,而是构造参数的语法糖,即等价于:

String str2(static_cast<String &&>(str1)); // 构造参数,所以是用来匹配函数参数的

那么std::move怎么实现的大概大家就有个数了:

template <typename T>
constexpr std::remove_reference_t<T> &&move(T &&ref) noexcept {
  return static_cast<std::remove_reference_t<T> &&>(ref);
}

一定一定要把握一个原则,std::move的本质是为了伪装,它并不能改变变量的左右性。也就是说,std::move(str1)并不能把 str 变成 rvalue,它本身是个变量,那么它就是 lvalue,一直都是。move 的作用仅仅在于,构造 str2 的时候能触发移动构造函数,仅此而已,其他的什么都没变。

那么也就是说,尽管我们用了「移动语义」来构造了 str2,但其实 str1 还是存在的,该是什么样还是什么样,并不会真的被「移动」。而此时的 str2 是 str1 的浅复制版本,原本的话它们的 buf_ 会指向同一片空间的,但因为我们在移动构造函数中强制把原来的对象 buf_ 给置空了,因此这里 str1 内部会出现空指针。所以这里有一次印证了「移动语义是软约束」这件事,使用之后行为如何,会不会出问题,完全取决代码怎么写。

一些实现细节

理解 std::move() 需要明白如下几点:

(1)引用折叠

  • 间接创建(只能间接,如类型别名或模板参数,语法不支持直接创建)引用的引用,这些引用会形成“折叠”
  • T& &、T& &&、T&& & 都会折叠成左值引用 T&
  • T&& && 折叠成右值引用 T&&

(2)模板右值引用参数

  • 模板函数形参类型是 T&&,而实参是一个左值,推断出的模板实参类型将是 T&,且函数参数将被实例化一个普通的左值引用参数 T&
  • 模板函数形参类型是 T&&,而实参是一个右值,推断出的模板实参类型将是 T,且函数参数将被实例化一个普通参数 T

(3)从一个左值 static_cast 到一个右值引用是允许的,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T

//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义T的类型别名为type
};

//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }

template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }

//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本

评论
  目录