Cpp / std::move 原理

零、功能和源码

std::move 是一个类型转换器,将左值转换成右值,其实现如下:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}

std::move 的实现还是挺简单的就这么几行代码,但要理解这几行代码可不容易。下面我们就来对它做下详细分析。

一、通用引用

首先我们来看一下 move 的输入参数,move 的输入参数类型称为通用引用类型。什么是通用引用呢?就是它既可以接收左值也可以接收右值。我们来看一下例子:

#include <iostream>

template <typename T>
void func(T &&param)
{
    std::cout << "the value is " << param << std::endl;
    return;
}

int main(int argc, char *argv[])
{
    int a = 123;

    auto &&b = 5; //通用引用,可以接收右值
    //int &&c = a; //错误,右值引用,不能接收左值

    auto &&d = a; //通用引用,可以接收左值
    //const auto &&e = a; //错误,加了const就不再是通用引用了

    func(a);  //通用引用,可以接收左值
    func(10); //通用引用,可以接收右值

    return 0;
}

通用引用成立条件:一种是 auto,另一种是通过模板定义的 T&&。实际上 auto 就是模板中的 T,它们是等价的。

下面我们就对这段代码做下详细解读。 代码中的 a 是个左值,因为它在内存中会分配空间,这应该没什么异义;b 是通过引用。为什么呢?因为通用引用有两个条件:一,必须是 T&& 的形式,由于 auto 等价于T,所以 auto && 符合这个要求;二,T 类型要可以推导,也就是说它必须是个模板,而 auto 是模板的一种变型,因此 b 是通用引用。通用引用即可以接收左值,也可以接收右值,所以 b = 5 是正确的;c 不是通用引用,因为它不符合 T&& 的形式。所经第三行代码是错误的,右值引用只能接收右值;d 是通用引用,所以给它赋值 a 是正确的;e 不是通用引用,它多了一个 const 已不符合 T&& 的形式,所以给它左值肯定会出错;最后两个函数调用的形参符合 T&&,又因是模板可以进行类型推导,所以是通用引用,因此给它传左值和右值它都能正确接收。

二、模板的类型推导

通用引用好强大呀!它既可以接收左值又可以接收右值,它是如何做到的呢?这就要讲讲模板的类型推导了。

模板的类型推导规则还是蛮复杂的,这里我们只简要说明一下,有兴趣的同学可以查一下 C++11 的规范。我们还是举个具体的例子吧:

template <typename T>
void func(ParamType param);

func(expr);

上面这个例子是函数模板的通用例子,其中 T 是根据 func 函数的参数推到出来的,而 ParamType 则是根据 T 推导出来的。T 与 ParamType 有可能相等,也可能不等,因为 ParamType 是可以加修饰的。我们看下面的例子:

template <typename T>
void f(T param);

template <typename T>
void func(T &param);

template <typename T>
void function(T &&param);

int main(int argc, char *argv[])
{

    int x = 10;        // x 是 int
    int &rr = x;       // rr 是 int &
    const int cx = x;  // cx 是 const int
    const int &rx = x; // rx 是 const int &
    int *pp = &x;      // pp 是 int *

    //下面是传值的模板,由于传入参数的值不影响原值,所以参数类型退化为原始类型
    f(x);  // T 是 int
    f(cx); // T 是 int
    f(rx); // T 是 int
    f(rr); // T 是 int
    f(pp); // T 是 int*,指针比较特殊,直接使用

    //下面是传引用模板, 如果输入参数类型有引用,则去掉引用;如果没有引用,则输入参数类型就是 T 的类型
    func(x);  // T 为 int
    func(cx); // T 为 const int
    func(rx); // T 为 const int
    func(rr); // T 为 int
    func(pp); // T 是 int*,指针比较特殊,直接使用

    //下面是通用引用模板,与引用模板规则一致
    function(x); // T 为 int&
    function(5); // T 为 int
}

上面代码中可以将类型推导分成两大类:其中类型不是引用也不是指针的模板为一类; 引用和指针模板为另一类。

对于第一类其推导时根据的原则是,函数参数传值不影响原值,所以无论你实际传入的参数是普通变量、常量还是引用,它最终都退化为不带任何修修饰的原始类型。如上面的例子中,const int &类型传进去后,退化为 int 型了。

第二类为模板类型为引用(包括左值引用和右值引用)或指针模板。这一类在类型推导时根据的原则是去除对等数量的引用符号,其它关键字照般。还是我们上面的例子,func(x)中 x 的类型为 int&,它与 T& 放在一起可以知道 T 为 int。另一个例子 function(x),其中 x 为 int& 它与 T&& 放在一起可知 T 为 int&

根据推导原则,我们可以知道通用引用最终的结果是什么了,左值与通用引用放在一起推导出来的 T 仍为左值,而右值与通用引用放在一起推导出来的 T 仍然为右值。

三、move 的返回类型

实际上上面通过模板推导出的 T 与 move 的返回类型息息相关的,要讲明白这一点我们先要把 move 的返回类型弄明白。下面我们就来讨论一下 move 的返回类型:

typename remove_reference<T>::type&&

move 的返回类型非常奇特,我们在开发时很少会这样写,它表示的是什么意思呢?

这就要提到 C++ 的另外一个知识点,即类型成员。你应该知道 C++ 的类成员有成员函数、成员变量、静态成员三种类型,但从 C++11 之后又增加了一种成员称为类型成员。类型成员与静态成员一样,它们都属于类而不属于对象,访问它时也与访问静态成员一样用::访问。

了解了这点,我们再看 move 的返类型是不是也不难理解了呢?它表达的意思是返回 remove_reference 类的 type 类型成员。而该类是一个模板类,所以在它前面要加 typename 关键字。

remove_reference 看着很陌生,接下来我们再分析一下 remove_reference 类,看它又起什么作用吧。其实,通过它的名子你应该也能猜个大概了,就是通过模板去除引用。我们来看一下它的实现吧。

template <typename T>
struct remove_reference
{
    typedef T type; //定义T的类型别名为type
};

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

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

上面的代码就是 remove_reference 类的代码,在 C++ 中 struct 与 class 基本是相同的,不同点是 class 默认成员是 private,而 struct 默认是 public,所以使用 struct 代码会写的更简洁一些。

通过上面的代码我们可以知道,经过 remove_reference 处理后,T 的引用被剔除了。假设前面我们通过 move 的类型自动推导得到 T 为 int&&,那么再次经过模板推导 remove_reference 的 type 成员,这样就可以得出 type 的类型为 int 了。

remove_reference 利用模板的自动推导获取到了实参去引用后的类型。现在我们再回过来看 move 函数的时候是不是就一目了解了呢?之前无法理解的 5 行代码现然变成了这样:

int &&move(int &&&&t)
{
    return static_cast<int &&>(t);
}
//或
int &&move(int &&&t)
{
    return static_cast<int &&>(t);
}

经上面转换后,我们看这个代码就清晰多了,从中我们可以看到move实际上就是做了一个类型的强制转换。如果你是左值引用就强制转换成右值引用。

四、引用折叠

上面的代码我们看起来是简单了很多,但其参数 int& && int && && 还是让人觉得很别扭。因为 C++ 编译器根本就不支持这两种类型。咦!这是怎么回事儿呢?

到这里我们就要讲到最后一个知识点引用折叠了。在C++中根本就不存 int& &&int && && 这样的语法,但在编译器内部是能将它们识别出来的。换句话说,编译器内部能识别这种格式,但它没有给我们提供相应的接口(语法)。

实际上,当编译器遇到这类形式的时候它会使用引用折叠技术,将它们变成我们熟悉的格式。其规则如下:

  • int & & 折叠为 int&
  • int & && 折叠为 int&
  • int && & 折叠为 int&
  • int && && 折叠为 int &&

总结一句话就是左值引用总是折叠为左值引用,右值引用总是折叠为右值引用。

经过这一系列的操作之后,对于一个具体的参数类型int & a,std::move 就变成了下面的样子:

int &&move(int &t)
{
    return static_cast<int &&>(t);
}

这一下我们就清楚它在做什么事儿了哈!

 

转载于:http://avdancedu.com/a39d51f9/

 

(SAW:Game Over!)

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值