理解 std::move
标准库move函数是使用右值引用的模板的一个很好的例子。
幸运的是,我们不必理解move所使用的模板机制也可以直接使用它。
但是,研究move是如何工作的可以帮助我们巩固对模板的理解和使用。
我们注意到,虽然不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。
由于move本质上可以接受任何类型的实参,因此我们不会惊讶于它是一个函数模板。
std:move是如何定义的
标准库是这样定义move的:
//在返回类型和类型转换中也要用到typename
template <typename T>
typename remove_reference<T>::typess move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
这段代码很短,但其中有些微妙之处。
首先,move的函数参数T&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。特别是,我们既可以传递给move一个左值,也可以传递给它一个右值,
string s1("hi!"),s2;
s2 = std::move(string("bye!"));// 正确:从一个右值移动数据
s2 = std::move(s1);// 正确:但在赋值之后,s1的值是不确定的
cout << s1 << endl;//没有打印任何东西
std::move是如何工作的
第一个赋值·
在第一个赋值中,传递给move 的实参是 string的构造函数的右值结果——string("bye!”)。
如我们已经见到过的,当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。
因此,在std::move(string("bye!"))中:
- 推断出的T的类型为string。
- 因此,remove reference用string进行实例化。
- remove reference<string>的type成员是string。
- move 的返回类型是string&&。
- move的函数参数t的类型为string&&。
因此,这个调用实例化move<string>,即函数
string&& move(string &&t)
函数体返回 static cast<string&&>(t)。t的类型已经是string&&,于是类型转换什么都不做。因此,此调用的结果就是它所接受的右值引用。
第二个赋值
现在考虑第二个赋值,它调用了std::move()。
在此调用中,传递给move的实参是一个左值。这样:
- 推断出的T的类型为string&(string的引用,而非普通string)。
- 因此,remove_reference用string&进行实例化。
- remove_reference<string&>的type成员是string。
- move 的返回类型仍是string&&。
- move的函数参数t实例化为string&&,会折叠为string&。
因此,这个调用实例化move<string&>,即
string&& move(string &t)
这正是我们所寻求的——我们希望将一个右值引用绑定到一个左值。
这个实例的函数体返回static cast<string&&>(t)。在此情况下,t的类型为string&,cast将其转换为string&&。
从一个左值static_cast到一个右值引用是允许的
通常情况下,static_cast只能用于其他合法的类型转换。
但是,这里又有一条针对右值引用的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用static cast显式地将一个左值转换为一个右值引用。
对于操作右值引用的代码来说,将一个右值引用绑定到一个左值的特性允许它们截断左值。
实际上,在某些情况下,确实可以使用static_cast
来将左值转换为右值引用,尽管这种做法并不常见,并且在实践中很少这样做,因为它会改变原有对象的值类别。
在C++中,static_cast
通常不用来直接将左值转换为右值引用,因为这种转换在语义上可能不太明确。然而,如果你确实需要这样做,并且了解这样做的后果,你可以使用static_cast
来显式地进行转换。
以下是一个例子,展示了如何使用static_cast
将一个左值转换为对应的右值引用:
#include <iostream>
void foo(int&& x) {
std::cout << "Called with rvalue reference to int: " << x << std::endl;
}
int main() {
int lvalue = 42; // 这是一个左值
foo(static_cast<int&&>(lvalue)); // 使用static_cast将左值强制转换为右值引用
return 0;
}
这段代码将左值lvalue
通过static_cast
转换为右值引用,并传递给函数foo
。尽管这是合法的,但通常不推荐这样做,因为它可能违反了右值引用的初衷,即处理临时对象或不再需要的资源。
需要注意的是,将左值强制转换为右值引用可能会导致未定义行为,特别是如果转换后的右值引用被用于移动语义,而原左值在之后仍被使用。这是因为移动操作通常会改变对象的内部状态,使得对象处于有效但未定义的状态。
因此,尽管技术上可以这样做,但实践中应该避免无必要地将左值转换为右值引用,除非在特定的上下文中,你完全理解这样做的后果,并且确信这是正确的处理方式。在大多数情况下,使用std::move
是更安全、更明确的选择,因为它清楚地表明了对象的值将被“移动”而不是“复制”或“引用”。
转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。
在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。
作为一个例子,我们将编写一个函数,它接受一个可调用表达式和两个额外实参。
我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它。
下面是我们的翻转函数的初步模样:
//接受一个可调用对象和另外两个参数的模板
//对“翻转”的参数调用给定的可调用对象
// flip1是一个不完整的实现:顶层const和引用丢失了
template <typename F, typename T1, typename T2>
void flip(F f, T1 t1, T2 t2)
{
f(t2,t1);
}
这个函数一般情况下工作得很好,但当我们希望用它调用一个接受引用参数的函数时就会出现问题:
void f(int v1, int& v2) //注意v2是一个引用
{
cout << v1 << " " << ++v2 << endl;}
在这段代码中, f改变了绑定到v2的实参的值。
但是,如果我们通过flip调用f,f所做的改变就不会影响实参
template <typename F, typename T1, typename T2>
void flip(F f, T1 t1, T2 t2)
{
f(t2,t1);
}
void f(int v1, int& v2) //注意v2是一个引用
{
cout << v1 << " " << ++v2 << endl;}
int i = 8;
int j = 0;
f(42, i);//f改变了实参i
flip(f, j, 42);//调用filp不会改变j
cout << j << endl;
问题在于j被传递给flip的参数t1。此参数是一个普通的、非引用的类型int,而非int&
因此,这个flip调用会实例化为
void flip1(void(*fcn)(int,int),int t1,int t2);
j的值被拷贝到t1中。f中的引用参数被绑定到t1,而非j,从而其改变不会影响j
定义能保持类型信息的函数参数
为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的“左造性”。
更进一步,可以想到我们也希望保持参数的const属性。
通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。
而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。
如果我们将函数参数定义为T1&&和T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性
template<typename F,typename Tl, typename T2>
void flip2(F f, Tl&& tl, T2&& t2)
{
f(t2,t1);
}
与较早的版本一样,如果我们调用flip2(f,j,42),将传递给参数t1一个左值j。
但是,在flip2中,推断出的T1的类型为int&,这意味着t1的类型会折叠为int&。
由于是引用类型,t1被绑定到j上。当flip2调用f时,f中的引用参数v2被绑定到t1,也就是被绑定到1。当f递增v2时,它也同时改变了j的值。
如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。
这个版本的flip2解决了一半问题。它对于接受一个左值引用的函数工作得很好,但不能用于接受右值引用参数的函数。
例如:
void g(int&& i, int& j)
{
cout << i << "" << j << endl;
}
如果我们试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。即使我们传递一个右值给flip2:
flip2(g,i,42);// 错误;不能从一个左值实例化 int&&
传递给g的将是flip2中名为t2的参数。函数参数与其他任何变量一样,都是左值表达式。
因此,flip2中对g的调用将传递给g的右值引用参数一个左值。
在调用中使用std::forward保持类型信息
我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。
类似于move,forward定义在头文件utility中。
与move不同,forward必须通过显式模板实参来调用。
forward 返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&
通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。
通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性
template <typename Type>
intermediary(Type &&arg)
{
//。。。finalFcn (std::forward<Type>(arg));
{
}
本例中我们使用Type作为forward的显式模板实参类型,它是从arg推断出来的。
由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。
如果实参是一个右值,则Type是一个普通(非引用)类型,forward<Type>将返回Type&&。
如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型。
在此情况下,返回类型是一个指向左值引用类型的右值引用。再次对 forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。
当用于一个指向模板参数类型的右值引用函数参数(T&a)时,forward会保持实参类型的所有细节。
使用forward,我们可以再次重写翻转函数:
template <typename F, typename Tl, typename T2>
void flip(F f, Tl &&t1, T2 &&t2)
{
f(std::forward<T2>(t2),std::forward<T1>(t1));
}
如果我们调用flip(g, i,42),i将以int&类型传递给g,42将以int&&类型传递给g。
与std::move相同,对std::forward不使用using声明是一个好主意。