> 文章列表 > c++中move和forward详解

c++中move和forward详解

c++中move和forward详解

文章目录

    • 前言
    • 模板参数推断
    • std::move
    • std::forward

前言

本文假定,我们已经明白左值和右值的区别。(可以认为:无法取地址的值,是左值;可以取地址的值,是右值;)

本文的目标是,搞懂std::move(移动)和std::forward(完美转发)的原理。

本文并不是一个很好的介绍文章,它的逻辑衔接不是很好。但是,每一节的开头都给出了参考链接,可以自行阅读。

本文是一个学习总结:搞懂模板参数的推断;move和forward只是,模板函数的参数是右值引用的应用。

  • move的作用是,将一个右值引用绑定到一个左值上(减少不必要的拷贝;左值不可直接再用)。
  • forward的作用是,完美转发(对象转发前后,类型保持不变)。

本文包含的内容:模板参数推断总结;std::move源码分析;std::forward源码分析;


模板参数推断

参考自:

  • 《C++ primer》第十六章 模板与泛型编程
  • Item 1:理解模板类型推导 - Effective Modern C++
fx(i) fx(ci) fx(5)
从左值引用函数参数推断类型
template <typename T> void f(T &P); i是一个int;模板参数类型T是int ci是一个const int;模板参数类型T是const int 错误:传递给一个&参数的实参必须时一个左值
template <typename T> void f2(const T &P); i是一个int;模板参数类型T是int ci是一个const int;模板参数类型T是int 一个const &参数可以绑定到一个右值;T是int
从右值引用函数参数推断类型
template <typename T> void f3(T &&P); i是一个int;模板参数类型T是int&,这是一个例外规则;
接着触发第二个列外,引用折叠,等效为f3(int &)
ci是一个const int;模板参数类型T是const int&;
同左边,最后等效为f3(const int&)
当然可以传递一个右值;模板参数类型T是int
普通的函数参数推断类型
template <typename T> void f4(T P); i是一个int;模板参数类型T是int ci是一个const int;模板参数类型T是int 模板参数类型T是int
  • 当函数模板类型参数是一个普通的左值引用时(template <typename T> void f(T &P);),绑定规则告诉我们,只能给它传递一个左值。
  • 当函数模板类型参数是一个包含const的的左值引用时(template <typename T> void f2(const T &P);),可以给它传递任何类型的实参–一个对象(const或非const)、一个临时对象,一个字面常量值。由于函数模板参数本身包含const,T的类型不会推断为const类型,const已经是函数参数类型的一部分了。
  • 当函数模板类型参数是一个右值引用时(template <typename T> void f3(T &&P);),当然可以给它传递一个右值。通常情况下,我们不能将一个右值引用绑定到一个左值上。这里有两个例外。(这两个例外是后面std::move和std::forward的基础。或者是为了实现move和forward,添加了这两个例外)
    • 例外一:如果我们将一个左值(如i),传递给实例的模板类型参数是右值引用时(如T&&),编译器会推断模板类型参数为实参的左值引用类型。如,这里我们调用f3(i),编译器推断T为int&,而非int。
    • 例外二:如果我们间接创建了一个引用的引用,则形成引用折叠。如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。(X& &、X& &&、X&& &都折叠成X&; 类型X&& && 折叠成X&&
  • 普通的函数模板参数做推断类型时(template <typename T> void f4(T P); ) ,采用值传递的方式。这意为着无论传递的是什么类型,都进行拷贝,形成一个新的对象。(传入int&, T类型也是int)
  • 上面并没有列出传入int &这种情况。因为这种情况,通过上面两个例外,会基本等同于上表。

std::move

参考自:

  • 《C++ primer》16.2.6 理解std::move
  • std::move - cppreference.com
  • c++ - What is std::move(), and when should it be used? - Stack Overflow

这个函数的作用是,将一个右值引用绑定到一个左值上。(将任意类型(通常是左值),转换成右值类型。)这样做的好处是,可以避免重复的拷贝。
例如,int rr1 = 1; int &&rr3 = std::move(rr1) 。调用std::move就意味着承诺:除了对rr1赋值和销毁它之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。(我们可以销毁一个移后源对象,也可以给它重新赋值,但不能使用一个移动后源对象的值。)

下面是一个move的一个使用示例。

#include <iostream>
#include <utility>
#include <vector>
#include <string>int main()
{std::string str = "Hello";std::vector<std::string> v;// 使用 push_back(const T&) 重载,// 表示我们将带来复制 str 的成本v.push_back(str);std::cout << "After copy, str is \\"" << str << "\\"\\n";// 使用右值引用 push_back(T&&) 重载,// 表示不复制字符串;而是// str 的内容被移动进 vector// 这个开销比较低,但也意味着 str 现在可能为空。v.push_back(std::move(str));std::cout << "After move, str is \\"" << str << "\\"\\n";std::cout << "The contents of the vector are \\"" << v[0]<< "\\", \\"" << v[1] << "\\"\\n";
}

输出如下:

After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"

此时,我们看下libstdc++: move.h Source File 的源码。分析下,为什么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); }

下面内容基本是来自《C++ primer》,书中解释的已经非常清晰了。

我们分析下面的函数过程。

string s1("hi"), s2;
s2 = std::move(string("bye")); // 传入一个右值
s2 = std::move(s1); // 传入一个左值

我们先来看下s2 = std::move(string("bye"));。move是一个参数为右值引用的模板函数,根据最上面的表格,我们可以得到下面内容:

  • 推断出的T为string类型。(T表示上面的_Tp,后文皆是如此)
  • 因此,remove_reference使用string进行实例化。
  • typename std::remove_reference<_Tp>::type&&,即使string&&。(使用typename是为了区别type是类型,而不是static成员变量;remove_reference作用是去除引用;remove_reference的type是去除引用后的类型)
  • 因此,move的返回类型,是string&&
  • static_cast<string&&>(string&&),正常运行。

我们再来考虑第二个赋值语句s2 = std::move(s1);。根据最上面的表格,我们可以得到下面内容:

  • 推断出T的类型为string&
  • 因此,remove_reference使用string&进行实例化。
  • remove_reference<_Tp>::type的成员是string
  • move的返回类型,仍然是string&&
  • 至于参数__t的类型,由于引用折叠,~~从T& &&~~折叠成T&
  • static_cast<string&&>(string&),则是将一个左值转换成右值。(更严谨些是,将左值转换成亡值–右值的一种。移后源之后不可用,可以销毁或者重新赋值。书上是这么说的,”将一个右值引用绑定到一个左值的特性允许它们截断左值,并且这种截断是安全的“。至于什么是”安全的截断“,不知道。)

std::forward

参考自:

  • c++ - What are the main purposes of std::forward and which problems does it solve? - Stack Overflow
  • C++中的万能引用和完美转发 | 阿振的博客
  • std::forward - cppreference.com

下面是cppreference中完美转发(forward)的示例。可见,传入的类型在完美转发后保持不变

#include <iostream>
#include <memory>
#include <utility>struct A {A(int&& n) { std::cout << "rvalue overload, n=" << n << "\\n"; }A(int& n)  { std::cout << "lvalue overload, n=" << n << "\\n"; }
};class B {
public:template<class T1, class T2, class T3>B(T1&& t1, T2&& t2, T3&& t3) :a1_{std::forward<T1>(t1)},a2_{std::forward<T2>(t2)},a3_{std::forward<T3>(t3)}{}private:A a1_, a2_, a3_;
};template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{return std::unique_ptr<T>(new T(std::forward<U>(u)));
}template<class T, class... U>
std::unique_ptr<T> make_unique2(U&&... u)
{return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}int main()
{   auto p1 = make_unique1<A>(2); // 右值int i = 1;auto p2 = make_unique1<A>(i); // 左值std::cout << "B\\n";auto t = make_unique2<B>(2, i, 3);
}

输出:

rvalue overload, n=2
lvalue overload, n=1
B
rvalue overload, n=2
lvalue overload, n=1
rvalue overload, n=3

我们来看下std::forward的源码:

  /*  @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);}

我没搞明白,为什么要分成两个函数。不过,最核心的是static_cast<_Tp&&>(__t)下面内容基本是翻译自上面的stackoverflow链接

基本上,给定表达式E(a, b, ... , c),我们希望表达式f(a, b, ... , c)与之等价。在 C++03 中,这是不可能的。有很多尝试,但都在某些方面失败了。

最简单的是使用左值引用:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{E(a, b, c);
}

但这无法处理临时值(右值):f(1, 2, 3);,因为它们不能绑定到左值引用。

下一次尝试可能是:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{E(a, b, c);
}

这解决了上述问题,因为const X&可以绑定到所有内容,包括左值和右值。

但这会导致新问题。它现在不允许E有非const参数:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); 
f(i, j, k); // oops! E cannot modify these

第三次尝试接受 const 引用,接下来const_cast去除const限制:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

这接受所有值,可以传递所有值,但可能导致未定义的行为:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); 
f(i, j, k); // ouch! E can modify a const object!

最终解决方案可以正确处理所有事情……但代价是无法维护。您提供 的重载f,以及const 和非常量的所有组合:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N 个论点需要 2N2^N2N个组合,这是一场噩梦。我们希望自动执行此操作。(这实际上是我们让编译器在 C++11 中为我们做的事情。)

解决方案是改用新添加的rvalue-references;我们可以在推导右值引用类型时引入新规则并创建任何想要的结果。(新规则,即是上表中的两个例外)

在代码中:

template <typename T>
void deduce(T&& x); int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

最后就是“转发”变量的值类别。请记住,一旦进入函数内部,参数就可以作为左值传递给任何东西:

void foo(int&);template <typename T>
void deduce(T&& x)
{foo(x); // fine, foo can refer to x
}deduce(1); // okay, foo operates on x which has a value of 1

那可不行。(这里有个不行的示例)。E 需要获得与我们获得的相同类型的值类别!解决方案是这样的:

static_cast<T&&>(x);

把这些放在一起给了我们“完美转发”:

template <typename A>
void f(A&& a)
{E(static_cast<A&&>(a)); 
}