一、通用引用:
通用引用(universal reference)是Scott Meyers在C++ and Beyond 2012演讲中自创的一个词,用来特指一种引用的类型。这种引用在源代码中(“T&&”)看起来像右值引用,但是它们可以表现左值引用(即“T&”)的行为。它们的双重性质允许它们绑定右值(就像右值引用那样)和左值(就像左值引用那样)。而且,它们可以绑定const或者非const对象,可以绑定volatile和非volatile对象,还可以绑定const和volatile同时作用的对象。它们实际上可以绑定任何东西。构成通用引用有两个条件:
(第一个例外)比如将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断,模板类型参数为:实参的左值引用类型
template<typename T> void f(T&&);
int i = 1;
f(i);//编译器推导出T为int&类型
f(10);//推导出T为普通类型int
二、引用折叠:
(第二个例外)通常不能直接定义引用的引用(引用非实体),但是通过类型别名或模板类型参数间接定义是可以的,但这时引用会形成“折叠”,例如上例中,当T被推导为int&
,f的参数类型变为int& &&
(无效代码,用于演示),引用折叠规则:
X& &
, X& &&
, X&& &
折叠为:X&
X&& &&
折叠为:X&&
于是最后我们得到的f的形参类型是int&
类型,即当我们用左值对f进行调用时,实际是用传引用的方式在调用函数。
三、std::move的工作原理:
由以上知识,就可以解释一下std::move的工作原理了:
template<typename T>
typename remove_reference::type&& move(T&& t)
{
return static_cast<
typename remove_reference::type&&>(t);
}
首先,move函数的参数类型是通用引用T&&
,可以绑定任意类型参数,其次,返回值是remove_reference
的type成员类型的右值引用,比如T被推导为int或者int&
,则remove_reference
为int类型,返回值类型为int&&,最后,函数体中static_cast内的转换过程类似,虽然不能隐式的将一个左值转换为一个右值引用,但是通过static_cast显式转换时允许的(把左值截断问题缩小在使用std::move代码的范围内)。
四、完美转发:
有时候,某些函数需要将其实参连同类型(const、左值、右值等属性)不变的转发给其他函数。
template<typename F,typename T>
void sender(F receiver, T t) //sender函数,接受一个可调用对象和一个模板参数类型的参数
{
receiver(t); //sender需要将自己的参数t转发给receiver函数
}
一般情况下这个函数能工作,但是当它调用一个接受引用类型参数的函数时就会有问题:
void rec(int& i) { ++i;}
int j = 1;
rec(j);
cout << j << endl; //输出j为2;
//但是通过sender调用时:
template<typename F,typename T>
void sender(F receiver, T t)
{
receiver(t);
}
sender(rec, j);
cout << j << endl; //输出j为1
其原因是j传递给sender函数,推断出T为int类型而非引用,j的值是被拷贝到形参t中的,因此对t值的改变不会反应到j中。
这时联想到我们讲的通用引用,将模板参数类型定义为T&&,接受左值时,T会被推断为左值引用类型,经过一次引用折叠,得参数t的类型为左值引用,它对应实参的const属性和左值、右值属性都将得到保持:
template<typename F, typename T>
void sender(F receiver, T&& t) //通用引用
{
receiver(t);
}
void rec(int& i)
{
++i;
}
sender(rec, j);
cout << j << endl; //OK!输出j为2
但是这儿又会遇到另一个问题:
template<typename F, typename T>
void sender(F receiver, T&& t)
{
receiver(t);
}
void rec(int&& i) //现在rec接受一个右值引用参数
{
++i;
}
int j = 1;
sender(rec,j);//错误:无法从一个左值实例化int&&
sender(rec,1);//错误:无法从一个左值实例化int&&
当我们试图对一个接受右值引用的函数转发参数时,会报以上错误,不论我们传递给sender函数的是一个左值还是右值。原因是传递给rec函数中形参i的是sender函数中的参数t,函数参数和其他变量一样都是左值表达式!所以会出现将左值绑定到右值引用的错误。
这时需要用到forward函数来保证:当sender函数接受一个右值实参,转发给rec函数时仍然能保持其右值属性。forward函数定义在
头文件中。
//forward函数
//lvalue (1)
template <typename Type> Type&& forward (typename remove_reference::type& arg) noexcept
{
return (static_cast(arg));
}
//rvalue (2)
template <typename Type> Type&& forward (typename remove_reference::type&& arg) noexcept
{
return (static_cast(arg));
}
forward函数的工作原理:由arg接受的实参类型推断出Type类型。
forward函数必须通过显式模板实参来调用,它跟通用引用配合可以保存原始实参的所有特性,回到我们的例子:
templateF, typename T>
void sender(F receiver, T&& t)
{
receiver(std::forward<T>(t));
}
当传递给sender的是一个右值——比如10时,推断出来的T是一个普通类型即int,传给forward的形参arg的实参就是一个int,从而推出Type是int,这时调用std::forward
返回的是int&&
,保存了原实参的右值属性。
当传递给sender的是一个左值——比如j时,这时我们推断出来的T应该是一个int&了,调用std::forward
返回int & &&
(无效代码,演示)折叠成为int&,保存了原实参的左值属性。
template<typename F, typename T>
void sender(F receiver, T&& t)
{
receiver(std::forward(t));
}
void rec1(int i)
{
++i;
}
void rec2(int& i)
{
++i;
}
void rec3(int&& i)
{
++i;
}
int main()
{
int i = 1, j = 1, k = 1;
/*
rec1(i);
cout << i << endl; //输出1
*/
/*
rec2(j);
cout << j << endl; //输出2
*/
/*
rec3(std::move(k));
cout << k << endl; //输出2
*/
sender(rec1, i);
cout << i << endl;//输出1
sender(rec2, j);
cout << j << endl;//输出2
sender(rec3, std::move(k));
cout << k << endl;//输出2
}