std::move()
首先,给出std::move函数的源代码:
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
看起来有点乱,去掉一些编译相关的宏和关键字,"精简后"的代码如下:
template<typename _Tp>
typename std::remove_reference<_Tp>::type&& move(_Tp&& __t)
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
从函数定义可以看出,无论参数是什么类型,最后返回的都是该传入参数的右值引用。其中,不太好理解的是以下两点:
一是函数传入参数的类型为_Tp&&,因为这里的模板参数_Tp的类型是不确定的,所以存在着类型推导,所以这里_Tp&&是万能引用。
二是函数体中remove_reference<_Tp>::type,其作用是去掉传入参数的引用,其后跟上&&,就成了传入参数的右值引用。
1._Tp&& 万能引用
万能引用其实通过两个简单的规则就能理解。
1.类型推导:
右值引用只能绑定到右值,如int&& ref_right = 10绑定到右值10,但是右值引用是函数模板中的函数参数时(如fun(Tp&& t), 该右值引用也可以绑定到左值 ,但是此时Tp的类型推导为左值引用,而不是左值。而将右值引用绑定到右值引用,则Tp的类型推导为普通类型(非引用)。
2.引用折叠:
首先,不能直接定义引用的引用,当间接(类型别名和模板参数)出现了引用的引用时,除了右值引用的右值引用,其他的都折叠为左值引用。如int& &&折叠为int&,int&& &折叠为int &,int&& &&折叠为int &&。
//类型别名引用折叠
using Type = int&&;
void test(Type &)
{
}
综合以上两条规则,可以给出如下代码:
template<typename _Tp>
void fun(_Tp&& __t)
{
}
int main()
{
int num1 = 10;
int& num2 = num1;
int&& num3 = 20;
fun(num1); //num1是左值,但是_Tp推导为int&,而不是int(例外规则),再根据引用折叠的规则, __t的类型推导为int&
fun(num2); //num2是左值引用,_Tp推导为int&,再根据引用折叠的规则, __t的类型推导为int&
fun(20); //20是右值,_Tp将会推导为int,再根据引用折叠的规则, __t的类型推导为int&&
fun(num3); //num3是右值引用,但是_Tp推导为int,而不是int&&,这里不是引用的引用,不需要引用折叠,__t的类型推导为int&&
}
2. std::remove_reference<_Tp>
所以通过万能引用接收参数后,函数模板中的_t变量不是左值引用就是右值引用。此时再通过remove_reference去掉其引用。
remove_reference的源码如下:
#if __has_builtin(__remove_reference)
template<typename _Tp>
struct remove_reference
{ using type = __remove_reference(_Tp); };
#else
template<typename _Tp> //主模板
struct remove_reference
{ using type = _Tp; };
template<typename _Tp>
struct remove_reference<_Tp&> //左值引用偏特化,偏特化也有叫部分具体化的
{ using type = _Tp; };
template<typename _Tp>
struct remove_reference<_Tp&&> //右值引用偏特化在上述例子不会被调用,因为之前推导过_Tp的类型只会被推导为int &或者int
{ using type = _Tp; };
#endif
也就是说,remove_reference是一个类模板(区别于函数模板,cpp中结构体也是类,仅成员访问权限不同)。
在没有内建的__remove_reference的情况下(else分支),通过左值引用偏特化的版本,获取到左值引用不带引用的类型,如remove_reference<int &>::type为int;通过主模板的版本,获取到不带引用的类型,如remove_reference<int>::type仍为int。
所以remove_reference<_Tp>::type&&一定是右值引用。但是需要注意尽量不要对纯右值使用move,这会返回一个临时变量的右值引用,可能有引用悬垂问题。尽管在g++与msvc上测试可能可以正常使用,但是最好不要依赖于编译器的实现来“解决”未定义问题。
最后,可以配合如下代码进行测试理解:
#include <iostream>
using namespace std;
//原始模板定义
template<typename _Tp>
struct my_remove_rerference{
using type = _Tp;
static void print(){
cout<<"调用原始模板!"<<endl;
}
};
//针对左值引用的特化版本
template<typename _Tp>
struct my_remove_rerference<_Tp&>{
using type = _Tp;
static void print(){
cout<<"调用左值引用!"<<endl;
}
};
//针对右值引用的特化版本
template<typename _Tp>
struct my_remove_rerference<_Tp&&>{
using type = _Tp;
static void print(){
cout<<"调用右值引用!"<<endl;
}
};
template<typename _Tp>
typename my_remove_rerference<_Tp>::type&&
my_move(_Tp&& __t) //my_move是对move的仿写,其返回值依然是右值引用,我们需要知道
{ //在my_remove_rerference<_Tp>到底是被
my_remove_rerference<_Tp>::print();
// my_remove_rerference<_Tp&>::print(); 可以测试Tp到底被推导何种类型1
// my_remove_rerference<_Tp&&>::print(); 可以测试Tp到底被推导何种类型2
return static_cast<typename my_remove_rerference<_Tp>::type&&>(forward<_Tp>(__t));
}
int main()
{
int left = 10;
int& left_ref = left;
int&& right_ref = 20;
my_move(left); /*传递左值,my_move函数中的参数:_Tp推导为int&,
其返回值my_remove_rerference<_Tp>中_Tp为int&,
所以调用类模板my_remove_rerference的左值引用版本,
*/
my_move(left_ref); /*传递左值引用,my_move函数中的参数:_Tp推导为int&,
其返回值my_remove_rerference<_Tp>中_Tp为int&,
所以调用类模板my_remove_rerference的左值引用版本
*/
my_move(20); /*传递纯右值,my_move函数中的参数:_Tp推导为int,
其返回值my_remove_rerference<_Tp>中_Tp仍为int,
所以调用类模板my_remove_rerference的原始模板版本
*/
my_move(move(right_ref)); /*传递右值引用,my_move函数中的参数:_Tp推导为int,
其返回值my_remove_rerference<_Tp>中_Tp仍为int,
所以调用类模板my_remove_rerference的原始模板版本
*/
return 0;
}