move vs forward
move vs forward
摘抄
c++ primer第五版,书真的好,所有的东西在里面都能找到
模板实参推断和引用
通用的规则:const是 底层的,不是顶层的。就是说 只要函数参数或者 类型有const最终推导出的就有const
-
从左值引用函数传参推断类型
对于
template<typename T> void f1(T&)
这要求实参必须是一个左值
f1(i); //模板类型是i
f1(ci); //ci
- 对于const 的左值引用形参
template<typename T> void f1(const T&)
可以传递任何类型,左值、右值、临时对象。推导出的类型不会带有const,const 已经是函数参数类型的一部分
- 从右值引用推导类型
template<typename T> void f1(T&& )
可以传递右值,T推导出的类型就是右值实参的类型
两个例外推导规则
-
当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板参数时,编译器推导模板类型参数为实参的左值引用类型
也就是
template<typename T> void f1(T&& )
int i;
f1(i);
参数的T 被推到为 int&
- 当编译器间接推导引用并产生引用折叠,会推导出不同的类型
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);
第一个是临时对象的转移
第二个是左值转换为右值并转移
-
对于第一个
_Tp的类型就是 string
然后返回值转换为了 string&&
-
对于第二个
实参为左值,那么_Tp的类型就是 string&
经过remove_reference解引用,还是 string&&
转发 std::forward
当我们在函数内部需要把参数原封不动来调用其他函数,我们需要机制确保这种要求,基本就是 和 模板类型&&配合,只有这种才不确定
默认的只要是一个变量,即使是右值引用类型,那么他也是左值变量,也只有匹配到左值的函数而不是右值,这就和我们期待的不同了
-
例子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)看出传递 接收类型是右值的变量,实际匹配的是左值
- 例子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也是解引用的作用,
- 一个右值传递进 T1&&,那么匹配出来的T1就是T1,但是进入函数内部参数就变成了 左值了,对应std::remove_reference<_Tp>::type&,参数就是 T1&,也就匹配到了这个,然后最终转发出来的是T1&&
- 一个左值传递进T1&&, 匹配出来T1就是T1&,叠加后是T1&,对应std::remove_reference<_Tp>::type&,参数就是 T1&,也就匹配到了这个,然后最终转发出来的是T1&
- forward(typename std::remove_reference<_Tp>::type&& __t),一般用不到,因为我们的forward都是配合万能引用,只有你强制给右值才会调用到这个,比如forward1
(std::move(v1)); forward1 (2); - 可以使用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。
即使参数是右值引用类型,但是变量都是 左值