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

前言

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

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

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

左值引用和右值引用

了解过基础的引用知识之后我们都知道左值引用的形式为 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 <iostream>
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 <iostream> // std::cout
#include <type_traits> // 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 被称为完美转发,听起来和 “万能引用”一样厉害,使用的头文件为 <utility>,在 /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 <iostream>
#include <utility>

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 的本质也是进行强制类型转换,形参为左值时返回左值引用,形参为右值时返回右值引用
  • 从定义入手可以理解很多花里胡哨的东西,透过现象看其本质。

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

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

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

  • 32
    点赞
  • 129
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论
std::move是C++11的一个标准库函数,用于将一个左值强制转换为右值引用。通过使用std::move,可以告诉编译器一个对象可以被移动而不是复制,从而提高程序的性能。std::move是一个类型转换函数,它不会真正移动数据,只是将左值转换成右值引用。 std::forward也是C++11的一个标准库函数,用于完美转发参数。当我们希望将一个函数的参数传递给另一个函数时,我们可以使用std::forward来保持参数的左右值属性。std::forward根据传入的参数类型来决定是将参数作为左值引用还是右值引用进行传递。 左右值引用是C++11引入的一个新的引用类型。左值引用指向一个具名的对象,而右值引用则可以绑定到一个临时对象或将要销毁的对象。左右值引用的一个重要应用是移动语义,通过将资源所有权从一个对象转移到另一个对象,避免了昂贵的资源拷贝操作。 移动构造函数是一种特殊的构造函数,用于在对象的移动操作进行资源移动而不是拷贝。在C++11,当一个对象被移动时,编译器会首先尝试调用其移动构造函数。移动构造函数需要一个右值引用作为参数,并将其它对象的资源移动到当前对象,然后将原来的对象置为有效的但未知的状态。 综上所述,C++11std::move和std::forward以及左右值引用与移动构造函数都是为了实现移动语义而引入的新特性。它们可以提高程序的性能,避免不必要的资源拷贝,以及实现更高效的对象移动操作。但是在使用时需要注意正确的使用方式和避免潜在的问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AlbertS

常来“玩”啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值