[C++中级进阶]001_C++0x里的完美转发到底是神马?

 问题描述

   C++无疑是十分强大的,但是你可知道,在C++0x标准出现之前,在C++界里有一个十分棘手而未能解决的问题——参数转发。问题的描述如下:

对于一个给定的函数E(a1, a2, ..., an),它有参数a1, a2, ..., an,你不可能写出一个函数F(a1, a2, ..., an),使得该函数与E(a1, a2, ..., an)完全等价。

   对这个问题进而拆分,它有两点:第一,函数F(a1, a2, ..., an)比如能够接受任意的参数列表,并在不改变参数性质的前提下,将参数传递给E(a1, a2, ..., an);第二,函数F(a1, a2, ..., an)必须能够将函数E(a1, a2, ..., an)的结果返回给自己的调用者。

   本文就第一点的参数转发进行讲解。

 标准解决方案的三大规则

   从更为严密的逻辑角度上来考虑转发的问题,我们的转发实现需要满足下面三个条件。这里假设函数F(a1, a2, ..., an)调用函数G(a1, a2, ..., an)

C1. 对于能使用函数F(a1, a2, ..., an)的地方,函数G(a1, a2, ..., an)也一定能使用。

C2. 对于不能使用函数F(a1, a2, ..., an)的地方,函数G(a1, a2, ..., an)也一定不能使用。

C3. 实现函数F(a1, a2, ..., an)的时候,复杂度必须是线性增加的。(这一点乍看起来可能不理解,现在可以不用理解,后面有实例说明)

 七种转发实现方案

   在实现转发的方案里,有七种需要我们了解,本文会为你一一介绍,这七种转发里,只有第七种能实现完美转发。

   七种转发中,1-4不需要对C++0x以前的标准进行修改,第五种转发需要对标准关于参数推倒的规则进行修改,第六种和第七种都用到了C++0x标准里的右值引用。

 方案1. 非常量左值引用转发

   何谓非常量左值引用呢?形如int& a;我们就把a叫做非常量左值引用。这里的左值引用就是我们平时使用的&。

   这个解决方案的实例代码如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 & a1, A2 & a2, A3 & a3)
3 {
4     return g(a1, a2, a3);
5 }
6 
7 void g(int a1, int a2, int a3){
8 
9 }

   转发失败原因:这个转发不能传入非常量右值,即下面的代码是无法通过编译的。

1 int main()
2 {
3     f(1, 2, 3);
4 }

   但是这种解决方案并不是一无是处的,对于那些只可能传入左值的场景来说,比如Boost库中的Iterator,这种应用还是能够看到的。只是它并不是一个完美的转发方案。

 方案2. 常量左值引用转发

   何谓常量左值引用呢?形如const int& a;我们就把a叫做常量左值引用。

   这个解决方案的实例代码如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4     return g(a1, a2, a3);
5 }
6 
7 void g(int& a1, int& a2, int& a3){
8  
9 }

   转发失败原因:上面的函数f虽然可以接受任何参数列表,但是当函数g接受非常量左值引用变量时,函数f是无法将常量左值引用参数传递给非常量左值引用的。

   这个解决方案一般用于拷贝构造函数,因为拷贝构造函数一般传递的都是形如const A&的参数,但也不排除有的拷贝构造函数比较变态,不传递const A&的参数。

 方案3. 非常量左值引用+常量左值引用转发

   这个解决方案的实例代码如下:

 1 template<class A1> 
 2 void f(A1 & a1)
 3 {
 4     return g(a1);
 5 }
 6 
 7 template<class A1> 
 8 void f(A1 const & a1)
 9 {
10     return g(a1);
11 }

   转发失败原因:上面的实现的确可以满足所有的参数都能传递了,并且当函数g接受非常量参数时,编译器也能找到最佳的匹配模板函数,即第一个。当然这个前提是所有的编译器达成共识,认定第一个模板函数。除此之外,还有一个重要的问题,当函数的参数有三个的时候,我们不得不像下面这样来实现我们的函数f:

 1 template<class A1, class A2, class A3> 
 2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
 3 {
 4     return g(a1, a2, a3);
 5 }
 6 
 7 template<class A1, class A2, class A3> 
 8 void f(A1 & a1, A2 const & a2, A3 const & a3)
 9 {
10     return g(a1, a2, a3);
11 }
12 
13 template<class A1, class A2, class A3> 
14 void f(A1 const & a1, A2 & a2, A3 const & a3)
15 {
16     return g(a1, a2, a3);
17 }
18 
19 template<class A1, class A2, class A3> 
20 void f(A1 & a1, A2 & a2, A3 const & a3)
21 {
22     return g(a1, a2, a3);
23 }
24 
25 template<class A1, class A2, class A3> 
26 void f(A1 const & a1, A2 const & a2, A3 & a3)
27 {
28     return g(a1, a2, a3);
29 }
30 
31 template<class A1, class A2, class A3> 
32 void f(A1 & a1, A2 const & a2, A3 & a3)
33 {
34     return g(a1, a2, a3);
35 }
36 
37 template<class A1, class A2, class A3> 
38 void f(A1 const & a1, A2 & a2, A3 & a3)
39 {
40     return g(a1, a2, a3);
41 }
42 
43 template<class A1, class A2, class A3> 
44 void f(A1 & a1, A2 & a2, A3 & a3)
45 {
46     return g(a1, a2, a3);
47 }

   是的,这是指数级别的增长,这个就不符合我们的C3规则了,呵呵,这下理解C3规则是什么含义了吧。

 方案4. 常量左值引用+const_cast转发

   const_cast的作用是什么呢?它可以去除常量的的const属性,这个转换可以解决方案2里遇到的问题。

   这个解决方案的实例代码如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4     return g(const_cast<A1 &>(a1), const_cast<A2 &>(a2), const_cast<A3 &>(a3));
5 }

   转发失败原因:很显然,去除了const属性,我们就能修改原来的常量了,这样的转发会造成对常量的修改。这里让我十分郁闷的是,C++既然提供了const,还非得提供一个const_cast,这是矛盾的存在。或许这就是C++的牛逼之处吧,哈哈。

 方案5. 非常量左值引用+修改的参数推倒规则转发

   这里说明一下,我在看胡健的博客的时候,对“修改的参数推倒”这句话费透了脑筋。乍一看不懂,仔细看还是不懂,基础不扎实可能吧。不过最后还是搞懂了,这里所谓的“修改参数推倒”是指修改C++的现有标准。在模板编程里,有一种参数推倒的说法。当你传递int类型的参数时,编译器会为你找到最佳匹配的模板函数,然后再把参数传递给这个模板函数。在方案1里,导致我们失败的事情就是无法传递非常量右值,但是如果修改C++标准,我们就能够将非常量右值推倒成常量右值。

   但是,修改标准后,对于现有用C++实现的代码造成十分巨大的破坏。具体参照如下代码:

 1 template<class A1> 
 2 void f(A1 & a1)
 3 {
 4     std::cout << 1 << std::endl;
 5 }
 6 
 7 void f(long const &)
 8 {
 9     std::cout << 2 << std::endl;
10 }
11 
12 int main()
13 {
14     f(5);              // 在既有参数推倒规则,会打印2;修改参数推倒规则后,会打印1
15     int const n(5);
16     f(n);              // 这种情况好一点,都会打印1
17 }

   看出来对现有代码的破坏了吗?注意注释。

 方案6. 右值引用转发

   何谓右值引用呢?这是C++0x里新追加的特性,如果想更清楚一点,可以参考胡健的博客。

   这个解决方案的实例代码如下:

1 template<class A1, class A2, class A3> 
2 void f(A1 && a1, A2 && a2, A3 && a3)
3 {
4     return g(a1, a2, a3);
5 }

   转发失败原因:函数g无法接收左值,因为不能将一个左值传递给一个右值引用。另外,当传递非常量右值时也会存在问题,因为此时a1、a2、a3本身是左值,这样当F的参数是非常量左值引用时,我们就可以来修改传入的非常量右值了,而右值是不能被修改的。

 完美方案7. 右值引用+修改的参数推倒规则转发

   你可能疑惑,不是说过修改参数推倒规则后会导致对既有代码的破坏吗?是的,不过那是对左值参数推倒规则的修改,我们这里要修改的是针对右值引用推倒规则的修改。首先,要理解参数推倒规则,我们要理解引用叠加规则:

1、T& + &         = T&

2、T& + &&       = T&

3、T&& + &       = T&

4、T或T&& + && = T&&

   如何验证上面的引用叠加规则呢?我们可以用下面这样一段代码来验证这个问题:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 typedef int&  LRINT;
 5 typedef int&& RRINT;
 6 
 7 int main(){
 8 
 9     int     a = 10;
10 
11     // 左值引用
12     LRINT   b = a;      // 单纯:&
13     LRINT&  c = a;      // 叠加:&  +  &   不能写做:LRINT&  c = 10;    可见c是左值引用
14 
15     // 右值引用
16     RRINT    d = 10;    // 单纯:&&
17     RRINT&&  e = 10;    // 叠加:&& + &&   不能写做:RRINT&& e = a;     可见e是右值引用
18     LRINT&&  f = a;     // 叠加:&  + &&   不能写做:LRINT&&  f = 10;   可见f是左值引用
19     RRINT&   g = a;     // 叠加:&& +  &   不能写做:RRINT&   g = 10;   可见g是左值引用
20 
21     system("pause");
22     return 0;
23 }

   理解了引用叠加规则后,让我们来看看修改后的针对右值引用的参数推倒规则:

   修改后的针对右值引用的参数推导规则为:若函数模板的模板参数为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)

   应用了新的参数推导规则后,我们来看下面的代码:

1 template<class A1> 
2 void f(A1 && a1)
3 {
4     return g(static_cast<A1 &&>(a1));
5 }

   当传给f一个左值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为左值引用类型T&,根据推导规则1,模板参数A被推导为T&。这样,在f内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T& &&>(a),根据引用叠加规则第2点,即为static_cast<T&>(a),这样转发给g的还是一个左值。

   当传给f一个右值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为右值引用类型T&&,根据推导规则2,模板参数A被推导为T。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T&&>(a),这样转发给F的还是一个右值(不具名右值引用是右值)。

   可见,使用该方案后,左值和右值都能正确地进行转发,并且不会带来其他问题。

 参考资料

   1.【原】C++ 11完美转发   http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html

   2.【原】C++ 11右值引用   http://www.cnblogs.com/hujian/archive/2012/02/13/2348621.html

   3. The Forwarding Problem: Arguments  http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1385.htm

   4. A Proposal to Add an Rvalue Reference to the C++ Language  http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html 

   5. A Brief Introduction to Rvalue References  http://www.artima.com/cppsource/rvalue.html

 

posted @ 2013-01-10 00:18  邵贤军  阅读(4069)  评论(5编辑  收藏  举报