[C/C++]关于C++11中的std::move和std::forward

深入解析C++0x中引入的std::move和std::forward函数,包括它们在GCC4.6中的具体实现及应用。文章详细阐述了引用折叠规则、模板参数推导、std::remove_reference元函数等核心概念,并通过代码实例展示了如何利用这些特性优化代码性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

std::move和std::forward是C++0x中新增的标准库函数,分别用于实现移动语义和完美转发。
下面让我们分析一下这两个函数在gcc4.6中的具体实现。

预备知识

  1. 引用折叠规则:
    X& + & => X&
    X&& + & => X&
    X& + && => X&
    X&& + && => X&&
  2. 函数模板参数推导规则(右值引用参数部分):
    当函数模板的模板参数为T而函数形参为T&&(右值引用)时适用本规则。
    若实参为左值 U& ,则模板参数 T 应推导为引用类型 U& 。
    (根据引用折叠规则, U& + && => U&, 而T&& <=> U&,故T <=> U& )
    若实参为右值 U&& ,则模板参数 T 应推导为非引用类型 U 。
    (根据引用折叠规则, U或U&& + && => U&&, 而T&& <=> U&&,故T <=> U或U&&,这里强制规定T <=> U )
  3. std::remove_reference为C++0x标准库中的元函数,其功能为去除类型中的引用。
    std::remove_reference<U&>::type <=> U
    std::remove_reference<U&&>::type <=> U
    std::remove_reference<U>::type <=> U
  4. 以下语法形式将把表达式 t 转换为T类型的右值(准确的说是无名右值引用,是右值的一种)
    static_cast<T&&>(t)
  5. 无名的右值引用是右值
    具名的右值引用是左值。
  6. 注:本文中 <=> 含义为“即,等价于“。

std::move


函数功能
std::move(t) 负责将表达式 t 转换为右值,使用这一转换意味着你不再关心 t 的内容,它可以通过被移动(窃取)来解决移动语意问题。

源码与测试代码
  1. template<typename _Tp>  
  2.   inline typename std::remove_reference<_Tp>::type&&  
  3.   move(_Tp&& __t)  
  4.   { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }  
  template<typename _Tp>
    inline typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t)
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
  1. #include<iostream>   
  2. using namespace std;  
  3.   
  4. struct X {};  
  5.   
  6. int main()  
  7. {  
  8.     X a;  
  9.     X&& b = move(a);  
  10.     X&& c = move(X());  
  11. }  
#include<iostream>
using namespace std;

struct X {};

int main()
{
	X a;
	X&& b = move(a);
	X&& c = move(X());
}
代码说明
  1. 测试代码第9行用X类型的左值 a 来测试move函数,根据标准X类型的右值引用 b 只能绑定X类型的右值,所以 move(a) 的返回值必然是X类型的右值。
  2. 测试代码第10行用X类型的右值 X() 来测试move函数,根据标准X类型的右值引用 c 只能绑定X类型的右值,所以 move(X()) 的返回值必然是X类型的右值。
  3. 首先我们来分析 move(a) 这种用左值参数来调用move函数的情况。
  4. 模拟单步调用来到源码第3行,_Tp&& <=> X&, __t  <=> a 。
  5. 根据函数模板参数推导规则,_Tp&& <=> X& 可推出 _Tp <=> X& 。
  6. typename std::remove_reference<_Tp>::type <=> X 。
    typename std::remove_reference<_Tp>::type&& <=> X&& 。
  7. 再次单步调用进入move函数实体所在的源码第4行。
  8. static_cast<typename std::remove_reference<_Tp>::type&&>(__t) <=> static_cast<X&&>(a)
  9. 根据标准 static_cast<X&&>(a) 将把左值 a 转换为X类型的无名右值引用。
  10. 然后我们再来分析 move(X()) 这种用右值参数来调用move函数的情况。
  11. 模拟单步调用来到源码第3行,_Tp&& <=> X&&, __t  <=> X() 。
  12. 根据函数模板参数推导规则,_Tp&& <=> X&& 可推出 _Tp <=> X 。
  13. typename std::remove_reference<_Tp>::type <=> X 。
    typename std::remove_reference<_Tp>::type&& <=> X&& 。
  14. 再次单步调用进入move函数实体所在的源码第4行。
  15. static_cast<typename std::remove_reference<_Tp>::type&&>(__t) <=> static_cast<X&&>(X())
  16. 根据标准 static_cast<X&&>(X()) 将把右值 X() 转换为X类型的无名右值引用。
  17. 由9和16可知源码中std::move函数的具体实现符合标准,
    因为无论用左值a还是右值X()做参数来调用std::move函数,
    该实现都将返回无名的右值引用(右值的一种),符合标准中该函数的定义。

std::forward


函数功能
std::forward<T>(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。

源码与测试代码
  1. /// forward (as per N3143)   
  2. template<typename _Tp>  
  3.   inline _Tp&&  
  4.   forward(typename std::remove_reference<_Tp>::type& __t)   
  5.   { return static_cast<_Tp&&>(__t); }  
  6.   
  7. template<typename _Tp>  
  8.   inline _Tp&&  
  9.   forward(typename std::remove_reference<_Tp>::type&& __t)   
  10.   {  
  11.     static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"  
  12.     " substituting _Tp is an lvalue reference type");  
  13.     return static_cast<_Tp&&>(__t);  
  14.   }  
  /// forward (as per N3143)
  template<typename _Tp>
    inline _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) 
    { return static_cast<_Tp&&>(__t); }

  template<typename _Tp>
    inline _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) 
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }
  1. #include<iostream>   
  2. using namespace std;  
  3.   
  4. struct X {};  
  5. void inner(const X&) {cout << "inner(const X&)" << endl;}  
  6. void inner(X&&) {cout << "inner(X&&)" << endl;}  
  7. template<typename T>  
  8. void outer(T&& t) {inner(forward<T>(t));}  
  9.   
  10. int main()  
  11. {  
  12.     X a;  
  13.     outer(a);  
  14.     outer(X());  
  15.     inner(forward<X>(X()));  
  16. }  
  17. //inner(const X&)   
  18. //inner(X&&)   
  19. //inner(X&&)  
#include<iostream>
using namespace std;

struct X {};
void inner(const X&) {cout << "inner(const X&)" << endl;}
void inner(X&&) {cout << "inner(X&&)" << endl;}
template<typename T>
void outer(T&& t) {inner(forward<T>(t));}

int main()
{
	X a;
	outer(a);
	outer(X());
	inner(forward<X>(X()));
}
//inner(const X&)
//inner(X&&)
//inner(X&&)
代码说明
  1. 测试代码第13行用X类型的左值 a 来测试forward函数,程序输出表明 outer(a) 调用的是 inner(const X&) 版本,从而证明函数模板outer调用forward函数在将参数左值 a 转发给了inner函数时,成功地保留了参数 a 的左值属性。
  2. 测试代码第14行用X类型的右值 X() 来测试forward函数,程序输出表明 outer(X()) 调用的是 inner(X&&) 版本,从而证明函数模板outer调用forward函数在将参数右值 X() 转发给了inner函数时,成功地保留了参数 X() 的右值属性。
  3. 首先我们来分析 outer(a) 这种调用forward函数转发左值参数的情况。
  4. 模拟单步调用来到测试代码第8行,T&& <=> X&, t  <=> a 。
  5. 根据函数模板参数推导规则,T&& <=> X& 可推出 T <=> X& 。
  6. forward<T>(t) <=> forward<X&>(t),其中 t 为指向 a 的左值引用。
  7. 再次单步调用进入forward函数实体所在的源码第4行或第9行。
  8. 先尝试匹配源码第4行的forward函数,_Tp <=> X& 。
  9. typename std::remove_reference<_Tp>::type <=> X 。
    typename std::remove_reference<_Tp>::type& <=> X& 。
  10. 形参 __t  与实参 t 类型相同,因此函数匹配成功。
  11. 再尝试匹配源码第9行的forward函数,_Tp <=> X& 。
  12. typename std::remove_reference<_Tp>::type <=> X 。
    typename std::remove_reference<_Tp>::type&& <=> X&& 。
  13. 形参 __t  与实参 t 类型不同,因此函数匹配失败。
  14. 由10与13可知7单步调用实际进入的是源码第4行的forward函数。
  15. static_cast<_Tp&&>(__t) <=> static_cast<X&>(t) <=> a。
  16. inner(forward<T>(t)) <=> inner(static_cast<X&>(t)) <=> inner(a) 。
  17. outer(a) <=> inner(forward<T>(t)) <=> inner(a)
    再次单步调用将进入测试代码第5行的inner(const X&) 版本,左值参数转发成功。
  18. 然后我们来分析 outer(X()) 这种调用forward函数转发右值参数的情况。
  19. 模拟单步调用来到测试代码第8行,T&& <=> X&&, t  <=> X() 。
  20. 根据函数模板参数推导规则,T&& <=> X&& 可推出 T <=> X 。
  21. forward<T>(t) <=> forward<X>(t),其中 t 为指向 X() 的右值引用。
  22. 再次单步调用进入forward函数实体所在的源码第4行或第9行。
  23. 先尝试匹配源码第4行的forward函数,_Tp <=> X 。
  24. typename std::remove_reference<_Tp>::type <=> X 。
    typename std::remove_reference<_Tp>::type& <=> X& 。
  25. 形参 __t  与实参 t 类型相同,因此函数匹配成功。
  26. 再尝试匹配源码第9行的forward函数,_Tp <=> X 。
  27. typename std::remove_reference<_Tp>::type <=> X 。
    typename std::remove_reference<_Tp>::type&& <=> X&& 。
  28. 形参 __t  与实参 t 类型不同,因此函数匹配失败。
  29. 由25与28可知22单步调用实际进入的仍然是源码第4行的forward函数。
  30. static_cast<_Tp&&>(__t) <=> static_cast<X&&>(t) <=> X()。
  31. inner(forward<T>(t)) <=> inner(static_cast<X&&>(t))  <=> inner(X())。
  32. outer(X()) <=> inner(forward<T>(t)) <=> inner(X())
    再次单步调用将进入测试代码第6行的inner(X&&) 版本,右值参数转发成功。
  33. 由17和32可知源码中std::forward函数的具体实现符合标准,
    因为无论用左值a还是右值X()做参数来调用带有右值引用参数的函数模板outer,
    只要在outer函数内使用std::forward函数转发参数,
    就能保留参数的左右值属性,从而实现了函数模板参数的完美转发。
更多 0

 

std::move是一个用于提示优化的函数,过去的c++98中,由于无法将作为右值的临时变量从左值当中区别出来,所以程序运行时有大量临时变量白白的创建后又立刻销毁,其中又尤其是返回字符串std::string的函数存在最大的浪费。

比如:

1 std::string fileContent = “oldContent”;
2 s = readFileContent(fileName);

因为并不是所有情况下,C++编译器都能进行返回值优化,所以,向上面的例子中,往往会创建多个字符串。readFileContent如果没有内部状态,那么,它的返回值多半是std::string(const std::string的做法不再被推荐了),而不是const std::string&。这是一个浪费,函数的返回值被拷贝到s中后,栈上的临时对象就被销毁了。

在C++11中,编码者可以主动提示编译器,readFileContent返回的对象是临时的,可以被挪作他用:std::move。

将上面的例子改成:

1 std::string fileContent = “oldContent”;
2 s = std::move(readFileContent(fileName));

后,对象s在被赋值的时候,方法std::string::operator =(std::string&&)会被调用,符号&&告诉std::string类的编写者,传入的参数是一个临时对象,可以挪用其数据,于是std::string::operator =(std::string&&)的实现代码中,会置空形参,同时将原本保存在中形参中的数据移动到自身。

不光是临时变量,只要是你认为不再需要的数据,都可以考虑用std::move移动。

比较有名的std::move用法是在swap中:

复制代码
1 template<typename T>
2 void swap(T& a, T& b)
3 {
4     T t(std::move(a));  // a为空,t占有a的初始数据
5     a = std::move(b); //  b为空, a占有b的初始数据
6     b = std::move(t); // t为空,b占有a的初始数据
7 } 
复制代码

总之,std::move是为性能而生的,正式因为了有了这个主动报告废弃物的设施,所以C++11中的STL性能大幅提升,即使C++用户仍然按找旧有的方式来编码,仍然能因中新版STL等标准库的强化中收益。

 

std::forward是用于模板编程中的,如果不需要编写通用的模板类和函数,可能不怎么用的上它。

要认识它的作用,需要知道C++中的几条规则:(这里有篇挺好的文章:http://blog.youkuaiyun.com/zwvista/article/details/6848582,但似乎因标准的更新,其中的规则已不完全成立了)

1. 引用折叠规则:

X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&

2. 对于模板函数中的形参声明T&&(这里的模板参数T,最终推演的结果可能不是一个纯类型,它可能还会带有引用/常量修饰符,如,T推演为const int时,实际形参为const int &&),会有如下规则:

如果调用函数时的实参为U&(这里的U可能有const/volatile修饰,但没有左/右引用修饰了),那么T推演为U&,显然根据上面的引用折叠规则,U& &&=>U&。

如果调用实参为U&&,虽然将T推导为U&&和U都能满足折叠规则(U&& &&=> U&&且U &&=>U&&),但标准规定,这里选择将T推演为U而非U&&。

总结一下第2条规则:当形参声明为T&&时,对于实参U&,T被推演为U&;当实参是U&&时,T被推演为U。当然,T和U具有相同的const/volatile属性。

3.这点很重要,也是上面zwvista的文章中没有提到的:形参T&& t中的变量t,始终是左值引用,即使调用函数的实参是右值引用也不例外。可以这么理解,本来,左值和右值概念的本质区别就是,左值是用户显示声明或分配内存的变量,能够直接用变量名访问,而右值主要是临时变量。当一个临时变量传入形参为T&& t的模板函数时,T被推演为U,参数t所引用的临时变量因为开始能够被据名访问了,所以它变成了左值。这也就是std::forward存在的原因!当你以为实参是右值所以t也应该是右值时,它跟你开了个玩笑,它是左值!如果你要进一步调用的函数会根据左右值引用性来进行不同操作,那么你在将t传给其他函数时,应该先用std::forward恢复t的本来引用性,恢复的依据是模板参数T的推演结果。虽然t的右值引用行会退化,变成左值引用,但根据实参的左右引用性不同,T会被分别推演为U&和U,这就是依据!因此传给std::forward的两个参数一个都不能少:std::forward<T>(t)。

 

再来,讨论一下,一个模板函数如果要保留参数的左右值引用性,为什么应该声明为T&&:

如果声明函数f(T t):实参会直接进行值传递,失去了引用性。

如果声明函数f(T &t): 根据引用折叠法则,无论T是U&还是U&&,T&的折叠结果都只会是U&,即,这个声明不能用于匹配右值引用实参。

如果声明函数f(T &&t): 如果T为U&,T&&的结果是U&,可以匹配左值实参;如果T为U&&,T&&的结果是U&&,可以匹配右值实参。又因为T的cv性和U相同,所以这种声明能够保留实参的类型信息。

 

先来看一组帮助类:

1 template<typename T> struct TypeName { static const char *get(){ return "Type"; } };
2 template<typename T> struct TypeName<const T> { static const char *get(){ return "const Type"; } };
3 template<typename T> struct TypeName<T&> { static const char *get(){ return "Type&"; } };
4 template<typename T> struct TypeName<const T&> { static const char *get(){ return "const Type&"; } };
5 template<typename T> struct TypeName<T&&> { static const char *get(){ return "Type&&"; } };
6 template<typename T> struct TypeName<const T&&> { static const char *get(){ return "const Type&&"; } };

在模板函数内部将模板参数T传给TypeName,就可以访问T的类型字符串:TypeName<T>::get()。

 

再一个帮助函数,用于打印一个表达式的类型:

1 template<typename T>
2 void printValType(T &&val)
3 {
4     cout << TypeName<T&&>::get() << endl;
5 }

注意3条规则在这个模板函数上的应用。规则1,解释了T&& val的声明足以保留实参的类型信息。规则2,说明了,当实参是string&时,T就是string&;当实参是const string&&时,T就是const string(而非const string&&)。规则3,强调,无论实参是string&还是string&&,形参val的类型都是string&!

注意TypeName<T&&>的写法,因为T只能为U&或者U,显然T&&可以根据折叠法则还原为实参类型U&和U&&。

 

这里是常见的const/左右引用组合的情形:

1 class A{}; // 测试类
2 A& lRefA() { static A a; return a;} // 左值
3 const A& clRefA() { static A a; return a;} // 常左值
4 A rRefA() { return A(); } // 右值
5 const A crRefA() { return A(); } // 常右值

测试一下上面的表达式类型:

1 printValType(lRefA());
2 printValType(clRefA());
3 printValType(rRefA());
4 printValType(crRefA());

输出依次是: Type&,const Type&,Type&&,const Type&&。

 

现在正式来探讨std::forward的实现。

回顾一下使用std::forward的原因:由于声明为f(T&& t)的模板函数的形参t会失去右值引用性质,所以在将t传给更深层函数前,可能会需要回复t的正确引用行,当然,修改t的引用性办不到,但根据t返回另一个引用还是可以的。恰好,上面的函数printValType是一个会根据实参类型不同,作出不同反映的函数,所以可以把它作为f的内层函数,来检测f有没有正确的修正t的引用行。

复制代码
 1 template<typename T>
 2 void f(T &&a)
 3 {
 4     printValType(a);
 5 }
 6 
 7 int main()
 8 {
 9     f(lRefA());
10     f(clRefA());
11     f(rRefA());
12     f(crRefA());
13 }
复制代码

输出:Type&,const Type&,Type&,const Type&。

可见后两个输出错了,这正是前面规则3描述的,当实参是右值引用时,虽然T被推演为U,但是参数a退化成了左值引用。

直接应用std::forward:

1 template<typename T>
2 void f(T &&a)
3 {
4     printValType(std::forward<T>(a));
5 }

输出:Type&,const Type&,Type&&,const Type&&。

输出正确了,这就是std::forward的作用啊。如果更深层的函数也需要完整的引用信息,如这里的printValType,那就应该在传递形参前先std::forward!

 

在编写自己的forward函数之前,先来尝试直接强制转化参数a:

1 template<typename T>
2 void f(T &&a)
3 {
4     printValType((T&&)a);
5 }

输出:Type&,const Type&,Type&&,const Type&&。

正确!因为不管T被推演为U&还是U,只要T&&肯定能还原为U&和U&&。

 

考虑下自己的forward函数应该怎么写:

 因为在forward的调用方中,形参已经丢失了右值引用信息,唯一的参考依据是T,要根据T还原为正确的参数,得T&&,因此,强制转换和返回类型都是T&&了,当然,forward还必须被以forward<T>()的方式显示指定模板类型,这样才能保证forward的模板参数T和上层函数f的T是相同类型。首先:

1 template<typename T>
2 T&& forward(... a)
3 {
4     return (T&&)a;
5 }

调用方f一定得显示指定类型forward<T>。

形参怎么写?形参a的类型由T构成,而且forward的实参一定是左值(暂时不考虑forward(std::string())的使用方法),也就是说,无论T是U&还是U,形参a的类型一定都得是U&,才能和实参匹配,所以,结果是:

1 template<typename T>
2 T&& forward(T& a)
3 {
4     return (T&&)a;
5 }

测试,输出:Type&,const Type&,Type&&,const Type&&。
正确!

 

再试下,如果f调用forward的时候,使用forward(a)的方式,没有显示指定模板类型会怎么样:

1 template<typename T>
2 void f(T &&a)
3 {
4     printValType(forward(a));
5 }

输出:T&&,const Type&&,Type&&,const Type&&。

错了。分析下,因为实参始终是左值,所以forward的形参T& a中,T就被推演为U,因此(T&&)a也就是(U&&)a所以结果错误。

为了避免用户使用forward(a),因此应该禁用forward的自动模板参数推演功能!可以借助std::identity,另外,将(T&&)换成static_cast<T&&>,规范一下:

1 template<typename T>
2 T&& forward(typename std::identity<T>::type& a)
3 {
4     return static_cast<T&&>(a);
5 }

 

上面讲的是针对T为U&或U,而实参始终为左值的情况,这是常见的情形;不过也有实参为右值的情况,还需要改进上面这个forward,但我这里就不写了。

这是我手里的gcc4.5.2的forward实现:

复制代码
 1   /// forward (as per N2835)
 2 /// Forward lvalues as rvalues.
 3   template<typename _Tp>
 4     inline typename enable_if<!is_lvalue_reference<_Tp>::value, _Tp&&>::type
 5     forward(typename std::identity<_Tp>::type& __t)
 6     { return static_cast<_Tp&&>(__t); }
 7 
 8   /// Forward rvalues as rvalues.
 9   template<typename _Tp>
10     inline typename enable_if<!is_lvalue_reference<_Tp>::value, _Tp&&>::type
11     forward(typename std::identity<_Tp>::type&& __t)
12     { return static_cast<_Tp&&>(__t); }
13 
14   // Forward lvalues as lvalues.
15   template<typename _Tp>
16     inline typename enable_if<is_lvalue_reference<_Tp>::value, _Tp>::type
17     forward(typename std::identity<_Tp>::type __t)
18     { return __t; }
19 
20   // Prevent forwarding rvalues as const lvalues.
21   template<typename _Tp>
22     inline typename enable_if<is_lvalue_reference<_Tp>::value, _Tp>::type
23     forward(typename std::remove_reference<_Tp>::type&& __t) = delete;
复制代码

第1/3版本就相当于我之前的实现,而版本2/4是实参为右值的情况,至于后者这种取舍的原因,还得去自己研究下使用场合和文档了。

我手里的vc2010实现的forward和我之前的实现相同,显然还不够,不过vc2010本来对标准也就还支持得少...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值