move vs forward

move vs forward

摘抄

c++ primer第五版,书真的好,所有的东西在里面都能找到

模板实参推断和引用

通用的规则:const是 底层的,不是顶层的。就是说 只要函数参数或者 类型有const最终推导出的就有const

  1. 从左值引用函数传参推断类型

    对于

    template<typename T>
    void f1(T&)
    

    这要求实参必须是一个左值

    f1(i); //模板类型是i

    f1(ci); //ci

    1. 对于const 的左值引用形参
    template<typename T>
    void f1(const T&)
    

    可以传递任何类型,左值、右值、临时对象。推导出的类型不会带有const,const 已经是函数参数类型的一部分

    1. 从右值引用推导类型
    template<typename T>
    void f1(T&& )
    

    可以传递右值,T推导出的类型就是右值实参的类型

两个例外推导规则

  1. 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板参数时,编译器推导模板类型参数为实参的左值引用类型

    也就是

    template<typename T>
    void f1(T&& )
    

    int i;

    f1(i);

    参数的T 被推到为 int&

    1. 当编译器间接推导引用并产生引用折叠,会推导出不同的类型

    X& &、X& && 、X&& &都将折叠为类型X&

    X&& &&,推导为X&&

万能引用

这个词是大家传出来的

啥意思呢就是:
template<typename T>
void f1(T&& );
这种模板 右值引用形式,可以传递任何参数给f1

  	1. 当函数参数是左值,那么推导出的T类型是 T&, 引用折叠后就是 T&
            	2. 当函数参数是右值,那么推导出的T类型就是T,函数参数是 T&&

但是这样对于编码就带来了不便,你不清楚 参数是左值还是右值,无法准确针对这个来写内部逻辑代码(左值涉及到拷贝构造、右值设计到移动构造)

适用场景:

模板转发

模板重载

理解std::move

使用std::move 获得绑定到左值的右值引用,模板的转换推导都是在编译期间完成的

看一下move的源码

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };

template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };

template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };

我们来两个例子看下:

    string str1 ="12";
    string str2 = std::move(string("12"));
    string str3 = std::move(str1);

第一个是临时对象的转移

第二个是左值转换为右值并转移

  1. 对于第一个

    _Tp的类型就是 string

    然后返回值转换为了 string&&

  2. 对于第二个

    实参为左值,那么_Tp的类型就是 string&

    经过remove_reference解引用,还是 string&&

转发 std::forward

当我们在函数内部需要把参数原封不动来调用其他函数,我们需要机制确保这种要求,基本就是 和 模板类型&&配合,只有这种才不确定

默认的只要是一个变量,即使是右值引用类型,那么他也是左值变量,也只有匹配到左值的函数而不是右值,这就和我们期待的不同了

  1. 例子1

    void f(int&& value) {
        std::cout <<  "void f(int&& value)" << std::endl;
    }
    
    void f(int& value) {
       std::cout <<  "void f(int& value)" << std::endl;
    }
    
    
    int main()
    {
        int&& test = 1;
        f(test);
    
        int test1 = 1;
        f(test1);
    
        f(1);
        return 0;
    }
    

    输出:

    void f(int& value)
    void f(int& value)
    void f(int&& value)

    看出传递 接收类型是右值的变量,实际匹配的是左值

    1. 例子2
    void f(int v1, int& v2) {
        std::cout << v1 << v2++ << std::endl;
    }
    
    template<typename F, typename T1, typename T2>
    void flip1(F f, T1 t1, T2 t2) {
        f(t1, t2);
    }
    int main()
    {
        int v1 = 1;
        int v2 = 2;
        f(v1, v2);
        flip1(f, v1, v2);
        return 0;
    }
    

    f的调用v2改变

    但是 flip1的调用 v2实际未发生改变,因为 f(int v1, int& v2)绑定到 T1 t1, T2 t2而不是实际的

解决办法:通过将函数参数定义为一个指向模板类型参数的右值引用,我们可以保持对应实参的所有类型信息。使用引用参数可以保持const

也就是
template<typename F, typename T1, typename T2>
void flip1(F f, T1&& t1, T2&& t2) {
    f(t1, t2);
}

传递左值给 flip1,推导为T1&,然后往下继续传递

那么右值呢?

void f(int &v1, int&& v2) {
    std::cout << v1 << v2++ << std::endl;
}

template<typename F, typename T1, typename T2>
void flip1(F f, T1&& t1, T2&& t2) {
    f(t1, t2);
}
int main()
{
    int v1 = 1;
    int v2 = 2;
    f(v1, v2);
    flip1(f, v1, 2);
    return 0;
}

就出现了,传递右值内部变成左值的问题

最终解决办法:std::forward

std::forward必须使用显示模板实参调用,返回值是T&&

就可以把上面的修改为

template<typename F, typename T1, typename T2>
void flip1(F f, T1&& t1, T2&& t2) {
    f(std::forward<T1>(t1), std::forward<T2>(t2));
}

看一下源码

 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);
    }

remove_reference也是解引用的作用,

  1. 一个右值传递进 T1&&,那么匹配出来的T1就是T1,但是进入函数内部参数就变成了 左值了,对应std::remove_reference<_Tp>::type&,参数就是 T1&,也就匹配到了这个,然后最终转发出来的是T1&&
  2. 一个左值传递进T1&&, 匹配出来T1就是T1&,叠加后是T1&,对应std::remove_reference<_Tp>::type&,参数就是 T1&,也就匹配到了这个,然后最终转发出来的是T1&
  3. forward(typename std::remove_reference<_Tp>::type&& __t),一般用不到,因为我们的forward都是配合万能引用,只有你强制给右值才会调用到这个,比如forward1(std::move(v1)); forward1(2);
  4. 可以使用std::cout << std::is_same<T1, int>::value << std::endl;去验证你的推理是否正确

利用了 解引用 + 模板匹配

这里多说一句forward(typename std::remove_reference<_Tp>::type&& __t),几乎很难遇到。因为就像我们知道的一个右值传递进 T1&&,那么匹配出来的T1就是T1,但是进入函数内部参数就变成了 左值了,对应std::remove_reference<_Tp>::type&,参数就是 T1&,也就匹配到了这个

SWAP

template<typename T>
void swap1(T& left, T& right) {
    T temp = std::move(right);
    right = std::move(left);
    left = temp;
}

利用移动的概念,轻松写出

关于转发参数包

我们在源码里面常常看见
 template<typename _Alloc, typename... _Args>
	shared_ptr(_Sp_make_shared_tag __tag, const _Alloc& __a,
		   _Args&&... __args)
	: __shared_ptr<_Tp>(__tag, __a, std::forward<_Args>(__args)...)
	{ }

这种使用就是准确的 把对应类型完美转发出去

总结

std::move 是为了把将亡值转换为右值,然后进行移动构造或者移动赋值,本质是强制类型转换

std::forward,一般搭配万能引用或者参数是右值引用使用,保证函数内部调用其他函数时候 类型正确,本质是 解引用 + 模板匹配,如果参数就是左值,那完全不需要考虑 std::forward。

即使参数是右值引用类型,但是变量都是 左值
posted @ 2021-05-25 17:12  make_wheels  阅读(84)  评论(0编辑  收藏  举报