http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html
【原】C++ 11完美转发
C++ 11中引入的一个非常重要也是比较难于理解的新特性就是完美转发(Perfect Forwarding)。完美转发中有两个关键词:“转发”和“完美”。
我们先来看第一个关键词“转发”,那么在C++中,“转发”表示什么含义呢?转发通常用在模板编程中,假设有一个函数F(a1, a2, ..., an),如果存在另一个函数G(a1, a2, ..., an),调用G相当于调用了F,则我们说函数G将a1, a2, ..., an等参数正确地转发给了函数F。再来看第二个关键词“完美”,“完美”转发是相对于其他转发方案而言的。在目前已提出的7种转发方案中,只有完美转发能够正确地实现转发的语义,其他方案都存在这样或那样的问题。下面一一进行介绍。
转发方案一:使用非常量左值引用。考虑下面的代码。
2 {
3 cout << a << endl;
4 }
5
6 template< class A>
7 void G(A &a)
8 {
9 F(a);
10 }
使用非常量左值引用时,我们可以调用F(10),但无法调用G(10),即我们无法接收非常量右值的参数。
转发方案二:使用常量左值引用。考虑下面的代码。
2 {
3 cout << a << endl;
4 }
5
6 template< class A>
7 void G( const A &a)
8 {
9 F(a);
10 }
使用常量左值引用时,函数G可以接收任意类型的值作为参数,包括非常量左值、常量左值、非常量右值和常量右值。但当F的参数类型为非常量左值引用时,我们无法将一个常量左值引用转发给一个非常量左值引用。
转发方案三:使用非常量左值引用 + 常量左值引用。考虑下面的代码。
2 void G(A &a)
3 {
4 F(a);
5 }
6
7 template< class A>
8 void G( const A &a)
9 {
10 F(a);
11 }
综合前面两种方案的分析结果,可以得出这种方案相当于对函数G进行了重载,此时可以接收任意类型的值作为参数,也可以顺利地实现转发。但由于使用了常量和非常量两种形式的重载,当参数的个数N较大时,需要重载的函数会呈指数级增长(2的N次方),因此这种方案实际上是不可取的。
转发方案四:使用常量左值引用 + const_cast。
2 void G( const A &a)
3 {
4 F(const_cast<A &>(a));
5 }
这种方案克服了方案二的缺点,现在可以将常量左值引用转发给非常量左值引用了。但这又带来了新的问题,假如F的参数是一个非常量左值引用,则调用G后,我们可以通过F来修改传入的常量左值和常量右值了,而这是非常危险的。
转发方案五:非常量左值引用 + 修改的参数推导规则。
这种方案与方案一类似,但需要修改现有的参数推导规则,即传递一个非常量右值给模板类型时,将它推导成常量右值,这样就解决了方案一中无法接收非常量右值的参数的问题。但由于修改了现有的参数推导规则,因此会导致已有代码的语义发生改变。考虑下面的代码。
2 void F(A &a)
3 {
4 cout << " void F(A& a) " << endl;
5 }
6
7 void F( const long &a)
8 {
9 cout << " void F(const long &a) " << endl;
10 }
在未修改参数推导规则前,调用F(10)会选择第二个重载函数,但修改后,却会调用第一个重载函数,这就给C++带来了兼容性的问题。
转发方案六:右值引用。考虑下面的代码。
2 void G(A &&a)
3 {
4 F(a);
5 }
在这种方案中,G将无法接收左值,因为不能将一个左值传递给一个右值引用。另外,当传递非常量右值时也会存在问题,因为此时a本身是一个左值,这样当F的参数是一个非常量左值引用时,我们就可以来修改传入的非常量右值了。
转发方案七:右值引用 + 修改的参数推导规则。
要理解修改后的参数推导规则,我们先来看一下引用叠加规则:
1、T& + & = T&
2、T& + && = T&
3、T&& + & = T&
4、T或T&& + && = T&&
修改后的针对右值引用的参数推导规则为:若函数模板的模板参数为A,模板函数的形参为A&&,则可分为两种情况讨论:
1、若实参为T&,则模板参数A应被推导为引用类型T&。(由引用叠加规则第2点T& + && = T&和A&&=T&,可得出A=T&)
2、若实参为T&&,则模板参数A应被推导为非引用类型T。(由引用叠加规则第4点T或T&& + && = T&&和A&&=T&&,可得出A=T或T&&,强制规定A=T)
应用了新的参数推导规则后,考虑下面的代码。
2 void G(A &&a)
3 {
4 F(static_cast<A &&>(a));
5 }
当传给G一个左值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为左值引用类型T&,根据推导规则1,模板参数A被推导为T&。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T& &&>(a),根据引用叠加规则第2点,即为static_cast<T&>(a),这样转发给F的还是一个左值。
当传给G一个右值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为右值引用类型T&&,根据推导规则2,模板参数A被推导为T。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T&&>(a),这样转发给F的还是一个右值(不具名右值引用是右值)。
可见,使用该方案后,左值和右值都能正确地进行转发,并且不会带来其他问题。另外,C++ 11为了方便转发的实现,提供了一个函数模板forward,用于参数的完美转发。使用forward后的代码可简化为:
2 void G(A &&a)
3 {
4 F(forward<A>(a));
5 }
为了便于进行各种转发方案的比较,下面以表格的形式列出了各自的特性。
转发方案 | 非常量左值 | 常量左值 | 非常量右值 | 常量右值 | 修改语言 | 已知问题 |
一、非常量左值引用 | 非常量左值 | 常量左值 | 无法转发 | 常量左值 | 否 | 无法接收非常量右值的参数 |
二、常量左值引用 | 常量左值 | 常量左值 | 常量左值 | 常量左值 | 否 | 无法将常量左值引用转发给非常量左值引用 |
三、非常量左值引用 + 常量左值引用 | 非常量左值 | 常量左值 | 常量左值 | 常量左值 | 否 | 重载函数过多,实际编码不可行 |
四、常量左值引用 + const_cast | 非常量左值 | 非常量左值 | 非常量左值 | 非常量左值 | 否 | 可修改常量左值和常量右值,不安全 |
五、非常量左值引用 + 修改的参数推导规则 | 非常量左值 | 常量左值 | 常量左值 | 常量左值 | 是 | 会导致兼容性问题,且不支持移动语义 |
六、右值引用 | 无法转发 | 无法转发 | 非常量左值 | 常量左值 | 是 | 可修改非常量右值,不安全 |
七、右值引用 + 修改的参数推导规则 | 非常量左值 | 常量左值 | 非常量右值 | 常量右值 | 是 | 暂无,故简称为完美转发 |
注:关于左值引用和右值引用,可以参考我的另一篇文章: 【原】C++ 11右值引用