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 void f(T &P); i是一个int;模板参数类型T是int ci是一个const int;模板参数类型T是const int 错误:传递给一个&参数的实参必须时一个左值
template void f2(const T &P); i是一个int;模板参数类型T是int ci是一个const int;模板参数类型T是int 一个const &参数可以绑定到一个右值;T是int
从右值引用函数参数推断类型
template void f3(T &&P); i是一个int;模板参数类型T是int&,这是一个例外规则;
接着触发第二个列外,引用折叠,等效为f3(int &)
ci是一个const int;模板参数类型T是const int&;
同左边,最后等效为f3(const int&)
当然可以传递一个右值;模板参数类型T是int
普通的函数参数推断类型
template void f4(T P); i是一个int;模板参数类型T是int ci是一个const int;模板参数类型T是int 模板参数类型T是int
  • 当函数模板类型参数是一个普通的左值引用时(template void f(T &P);),绑定规则告诉我们,只能给它传递一个左值。
  • 当函数模板类型参数是一个包含const的的左值引用时(template void f2(const T &P);),可以给它传递任何类型的实参–一个对象(const或非const)、一个临时对象,一个字面常量值。由于函数模板参数本身包含const,T的类型不会推断为const类型,const已经是函数参数类型的一部分了。
  • 当函数模板类型参数是一个右值引用时(template void f3(T &&P);),当然可以给它传递一个右值。通常情况下,我们不能将一个右值引用绑定到一个左值上。这里有两个例外。(这两个例外是后面std::move和std::forward的基础。或者是为了实现move和forward,添加了这两个例外)
    • 例外一:如果我们将一个左值(如i),传递给实例的模板类型参数是右值引用时(如T&&),编译器会推断模板类型参数为实参的左值引用类型。如,这里我们调用f3(i),编译器推断T为int&,而非int。
    • 例外二:如果我们间接创建了一个引用的引用,则形成引用折叠。如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。(X& &、X& &&、X&& &都折叠成X&; 类型X&& && 折叠成X&&
  • 普通的函数模板参数做推断类型时(template 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 
#include 
#include 
#include 
 
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&&),正常运行。

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

  • 推断出T的类型为string&
  • 因此,remove_reference使用string&进行实例化。
  • remove_reference<_Tp>::type的成员是string
  • move的返回类型,仍然是string&&
  • 至于参数__t的类型,由于引用折叠,~~从T& &&~~折叠成T&
  • static_cast(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 
#include 
#include 
 
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 个论点需要 2 N 2^N 2N个组合,这是一场噩梦。我们希望自动执行此操作。(这实际上是我们让编译器在 C++11 中为我们做的事情。)

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

在代码中:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce(int& &&) -> deduce(int&)
deduce(1); // deduce(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)); 
}

你可能感兴趣的:(#,c/c++编程,c++,move,forward,模板参数)