C++11中std::move和std::forward到底干了啥

文章目录

  • 前言
  • 左值引用和右值引用
  • std::move
    • std::remove_reference
    • static_cast
    • std::move 到底干了啥
  • std::forward
    • 必要性
    • 疑惑
    • std::forward 到底干了啥
  • 总结

前言

C++11中的右值引用的出现,在特定情况下减少了对象的拷贝,提升了C++程序的效率,伴随而来的 std::movestd::forward 也大量出现在程序代码中,但是这两个函数究竟干了啥呢?其实他们的本质都是转换函数,也就是完成左值和右值之间的转换,需要注意的是左值可以转换成右值,但是右值无法转换成左值。

关于左值、右值、左值引用和右值引用的概念可以看看之前的总结:

  • 简单聊聊C/C++中的左值和右值
  • C++11在左值引用的基础上增加右值引用

虽然温故不一定知新,但绝对可以增强记忆,本章的内容说起来很绕,我也是边学边总结,有不对的地方还请大佬们指出来。

左值引用和右值引用

了解过基础的引用知识之后我们都知道左值引用的形式为 T& t,一般会像成下面这样使用:

class A{

private:
    int n;
};

void test(A& obj) {
    //...
}

A obj;
test(obj);

而右值引用是在左值引用的基础上多加一个&,形式变为 T&& t,使用方式如下:

class A{

private:
    int n;
};

void test(A&& obj) {
    //...
}

test(A());

这种通过 & 的个数区分左值引用和右值引用的方法,在大多数的普通函数中没有问题,但是放到模板参数或者 auto 关键字之后的位置就不太灵了,因为这些地方会推导实际的类型,正是有了参数推导,才使得模板中出现了“万能引用”的说法,也就是下面这样:

#include 
using namespace std;

template<typename T>
void func(T&& val)
{
    cout << val << endl;
}

int main()
{
    int year = 2020;
    func(year);
    func(2020);
    return 0;
}

函数 func 即能接受变量 year 这样的左值作为参数,也能接受 2020 这样的常数作为右值,简直太完美。那么这里是怎样推导的呢?这就要请出一个引用的“折叠”规则了,描述如下:

A& & 折叠成 A&
A& && 折叠成 A&
A&& & 折叠成 A&
A&& && 折叠成 A&&

根据这个规则,func 函数在接受 year 作为参数时应该是一个左值引用,那么模板参数 T 会被推到为 A& 与后面的 && 折叠为 A&,接受 year 没问题。而这个函数在接受 2020 作为参数时应该是一个右值引用,那么模板参数 T 会被推导成 A,与后面的 && 形成 A&&,可以接受右值,知道了这些基础知识我们接着往后看。

std::move

这个函数听起来好像是一个小人移动时调用的函数,但它却是一个把左值转化成右值的转化函数,我们看一下 std::move 函数的实现:

  /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

这是一个模板函数,一共才4行,好像最麻烦的就是这个 std::remove_reference<_Tp>::type&& 了,先来看看它是什么,其实它的作用就是,移除类型的引用,返回原始类型。

std::remove_reference

它的可能实现如下:

template <typename T>
struct remove_reference {
    using type = T;
};

template <typename T> // 模板特化
struct remove_reference<T&> {
    using type = T;
};

template <typename T> // 模板特化
struct remove_reference<T&&> {
    using type = T;
};

它的作用可以参考 cppreference.com - remove_reference,示例如下:

#include  // std::cout
#include  // std::is_same

template<class T1, class T2>
void print_is_same() {
  std::cout << std::is_same<T1, T2>() << '\n';
}

int main() {
  std::cout << std::boolalpha;

  print_is_same<int, int>();
  print_is_same<int, int &>();
  print_is_same<int, int &&>();

  print_is_same<int, std::remove_reference<int>::type>();
  print_is_same<int, std::remove_reference<int &>::type>();
  print_is_same<int, std::remove_reference<int &&>::type>();
}

运行结果

true
false
false
true
true
true

从这个例子可以清晰的看出 std::remove_reference 就是返回去掉引用的原始类型。

static_cast

明白了上面 std::remove_reference 的作用,整个 std::move 函数就剩下一个 static_cast 函数了,其实到这里也就清晰了,std::move 函数的作用就先通过 std::remove_reference 函数得到传入参数的原始类型 X,然后再把参数强转成 X&& 返回即可,参数的 _Tp 的推导参考引用折叠规则。

std::move 到底干了啥

通过前面的一通分析我们发现,std::move 的内部只做了一个强制类型转换,除此之外啥也没干,其实就是对传入的参数重新解释了一下,并没有实质性的动作。

那么为什么要使用 std::move 这个名字呢?这个名字更多的是起到提醒的作用,告诉使用者这里可能进行了到右值的转化,相关的对象后续可能发生移动,“被掏空”了,如果你继续使用这个对象,行为是未定义的,后果自负。

std::forward

std::forward 被称为完美转发,听起来和 “万能引用”一样厉害,使用的头文件为 ,在 /usr/include/c++/5/bits/move.h 文件中的定义如下:

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

std::forward 用于函数模板中完成参数转发任务,我们必须在相应实参为左值,该形参成为左值引用时把它转发成左值,在相应实参为右值,该形参成为右值引用时把它转发成右值。

有了前面的铺垫我们直接来分析代码吧,第一个版本接受参数苏为左值引用的情况,因为 std::remove_reference<_Tp>::type_Tp 的原始类型,所以 t 就是左值引用类型,调用这个函数时,_TpX& 类型,经过引用这的 _Tp&& => X& && => X&,所以返回值也是左值引用。

同理,第二个版本接受右值引用参数,返回值也是一个右值引用。

从目前的情况来看,std::forward 好像什么也没做,只是将参数强转以后返回,如果不使用这个函数会有什么问题呢?

必要性

为什么要使用 std::forward 我们可以通过一个例子来看一下:

#include 
#include 

void Print(int& val) {
    std::cout << "lvalue refrence: val=" << val << std::endl;
}

void Print(int&& val) {
    std::cout << "rvalue refrence: val=" << val << std::endl;
}

template<typename T>
void TPrint(T &&t) {
    return Print(t);
}

int main() {
    int date = 1021;
    TPrint(date);
    TPrint(501);

    return 0;
}

看到这个例子可以先思考一下,运行结果会是什么呢?可能和你想的有点不一样哦,看看下面的答案:

lvalue refrence: val=1021
lvalue refrence: val=501

有点出乎意料啊,为什么 Print(int&& val) 这个函数没有被调用呢?原因在于“右值引用是一个左值”,很懵对不对,接着往下看:

int i = 101;
int& li = i;

int&& ri = 120;

这段代码中哪些是左值,哪些是右值呢?可以肯定的是 ili 是左值, 101120 是右值,而ri也是左值,因为它也一个可以取地址并长期有效的变量啊,只不过这个左值引用了一个右值而已。

接着回到刚才的例子,TPrint(501); 调用模板函数时,T被推导为 int,所以模板被实例化为:

void TPrint(int&& t) {
    return Print(t);
}

运行到这里,t 实际上是一个左值,所以调用了 void Print(int& val) 这个函数,那么怎样才能调用 void Print(int&& val) 这个版本呢?是时候请出 std::forward 函数了,将模板函数进行如下修改:

template<typename T>
void TPrint(T&& t) {
    return Print(std::forward<T>(t));
}

修改之后再来分析一下,TPrint(501); 调用模板函数时,T被推导为 int,所以模板被实例化为:

void TPrint(int&& t) {
    return Print(std::forward<int>(t));
}

这里会调用 std::forward 的这个版本:

  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

函数的返回类型为 int&&,然后就调用了 void Print(int&& val) 这个版本的打印函数。

疑惑

可能有人会说,这不对啊,使用 std::forward 修改之前函数参数就是 int&& 类型,修改之后得到的返回值还是 int&& 类型,这有什么区别吗?

这里的区别就在于,使用 std::forward 之前的 int&& 是有名字的变量 t,它是一个左值,而使用 std::forward 之后的 int&& 是有个匿名变量,它是一个右值,真正的差距就在这里。

std::forward 到底干了啥

它和 std::move 一样,std::forward 也是做了一个强制类型转换,当形参成为左值引用时把它转换成左值引用返回,当形参成为右值引用时把它转换成右值引用返回。

总结

  • std::move 并没有实际的“移动”操作,只是在内部进行了强制类型转换,返回一个相关类型的右值引用
  • std::move 的名字主要标识它后续可能会被其他人“掏空”,调用它之后如果继续使用,行为未定义,后果自负
  • std::forward 的本质也是进行强制类型转换,形参为左值时返回左值引用,形参为右值时返回右值引用
  • 从定义入手可以理解很多花里胡哨的东西,透过现象看其本质。

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

日拱一卒无有尽,功不唐捐终入海~

我们追求的样子:十分沉静,九分气质,八分资产,七分现实,三分颜值,二分糊涂,一份自知之明。

你可能感兴趣的:(C++11/17/20新特性,C++,c++,move,forward,static_cast,左值右值)