关于c++11的一些特性(2) 完美转发

Tags:

本文测试环境:

系统:Linux ubuntu 4.2.0-16-generic #19-Ubuntu SMP x86_64 GNU/Linux

gcc版本: gcc version 5.2.1 20151010 (Ubuntu 5.2.1-22ubuntu2)

神奇的emplace_back函数

使用std::vector时,要么存储的是指针类型,要么是值类型。指针类型是指,我把一个对象放在别的地方,比如说堆内存,然后把这个对象的内存地址放在vector里;值类型是值,我不把对象放别的地方了,而是直接放到vector自己的内存空间里。

对于值类型的情况,要考虑一个问题:往vector插入对象,这个操作可能开销会很大。

比如看下面这段测试代码:

 1 #include <stdio.h>
 2 #include <vector>
 3 using namespace std;
 4 
 5 
 6 class Item {
 7 public:
 8     char name;
 9     int val;
10 public:
11     ~Item() {
12         printf("[dtor called] (%c, %i)\n", name, val);
13     }
14     Item() :name('_'), val(0) {
15         printf("[default ctor called] \n");
16     }
17 
18     Item(char n, int v) :name(n), val(v) {
19         printf("[ctor called] (%c, %i)\n", name, val);
20     }
21     Item(Item&& a) {
22         printf("[move ctor called] (%c, %i)\n", a.name, a.val);
23         name = a.name;
24         val = a.val;
25     }
26 };
27 
28 
29 int main() {
30     vector<Item> v1;
31     for (int i = 0; i < 3; i++) {
32         v1.emplace_back('a', i);
33     }
34     printf("-----------------------\n");
35     vector<Item> v2;
36     v2.reserve(10);
37     for (int i = 0; i < 3; i++) {
38         v2.emplace_back('b', i);
39     }
40     printf("-----------------------\n");
41     vector<Item> v3;
42     v3.push_back({ 'c', 3 });
43     printf("-----------------------\n");
44     return 0;
45 }

编译:

gcc test.cpp -o test.out -std=gnu++11 -lstdc++

运行结果:

 1 [ctor called] (a, 0)
 2 [move ctor called] (a, 0)
 3 [dtor called] (a, 0)
 4 [ctor called] (a, 1)
 5 [move ctor called] (a, 0)
 6 [move ctor called] (a, 1)
 7 [dtor called] (a, 0)
 8 [dtor called] (a, 1)
 9 [ctor called] (a, 2)
10 -----------------------
11 [ctor called] (b, 0)
12 [ctor called] (b, 1)
13 [ctor called] (b, 2)
14 -----------------------
15 [ctor called] (c, 3)
16 [move ctor called] (c, 3)
17 [dtor called] (c, 3)
18 -----------------------
19 [dtor called] (c, 3)
20 [dtor called] (b, 0)
21 [dtor called] (b, 1)
22 [dtor called] (b, 2)
23 [dtor called] (a, 0)
24 [dtor called] (a, 1)
25 [dtor called] (a, 2)

观察发现:

  • 第一段测试,有多余的函数调用:move构造函数以及析构函数
  • 第二段测试,没有多余的调用
  • 第三段测试,有多余的函数调用:move构造函数以及析构函数

所以第二种写法是性能最好的。能够直接在vector的内存空间中构造对象。其他写法都会生成临时对象。

然而实际编程中,并不是总能这样子写,因为reverse的参数该填多少,需要细心考虑;如果vector存的是基类指针类型,那么上面任意一种写法差别都不大(最多拷贝一个指针地址而已)。

这些问题另当别论,现在回到本文主题上。

这个例子中的:v2.emplace_back('b', i),其实就是用完美转发实现的。

Imperfect forwarding

理解完美转发之前,先搞懂什么是不完美转发。下面会用一些测试代码来分析一下。

前置说明:

func是随便写的一个普通函数;wrapper是对func的一层封装;测试过程是控制变量法,针对特定的wrapper函数写法,不断修改func的参数的类型以及wrapper的调用方式,测试程序是否可以编译并且wrapper函数是否能够正确完成作为一个“封装函数”的基本要求。

第1组测试:func参数为 const int p

 1 void func(const int p) {
 2 }
 3 
 4 int number99(){
 5     return 99;
 6 }
 7 
 8 template <typename T>
 9 void wrapper(T& p) { func(p); }
10 
11 //used for switching the two test cases below
12 #define TEST_FUNC
13 
14 #if defined(TEST_FUNC)
15 
16 void test_func(){
17     int a = 1;
18     const int b = 1;
19     int& c = a; 
20     const int& d = a;
21     func(a); // ok
22     func(b); // ok
23     func(c); // ok
24     func(d); // ok
25     func(1); // ok
26     func(number99()); // ok
27 }
28 
29 #else
30 
31 void test_wrapper(){
32     int a = 1;
33     const int b = 1;
34     int& c = a; 
35     const int& d = a;
36     wrapper(a); // ok
37     wrapper(b); // ok
38     wrapper(c); // ok
39     wrapper(d); // ok
40     wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
41     wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
42 }
43 
44 #endif

第1组测试,wrapper函数就和func表现得不一致了。

第2组测试:func参数为 int p

 1 void func(int p) {
 2 }
 3 
 4 int number99(){
 5     return 99;
 6 }
 7 
 8 template <typename T>
 9 void wrapper(T& p) { func(p); }
10 
11 //used for switching the two test cases below
12 #define TEST_FUNC
13 
14 #if defined(TEST_FUNC)
15 
16 void test_func(){
17     int a = 1;
18     const int b = 1;
19     int& c = a; 
20     const int& d = a;
21     func(a); // ok
22     func(b); // ok
23     func(c); // ok
24     func(d); // ok
25     func(1); // ok
26     func(number99()); // ok
27 }
28 
29 #else
30 
31 void test_wrapper(){
32     int a = 1;
33     const int b = 1;
34     int& c = a; 
35     const int& d = a;
36     wrapper(a); // ok
37     wrapper(b); // ok
38     wrapper(c); // ok
39     wrapper(d); // ok
40     wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
41     wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
42 }
43 
44 #endif

结果和第1组一样。

第3组测试:func参数为 int& p

 1 void func(int& p) {
 2 }
 3 
 4 int number99(){
 5     return 99;
 6 }
 7 
 8 template <typename T>
 9 void wrapper(T& p) { func(p); }
10 
11 #define TEST_FUNC
12 
13 #if defined(TEST_FUNC)
14 
15 void test_func(){
16     int a = 1;
17     const int b = 1;
18     int& c = a; 
19     const int& d = a;
20     func(a); // ok
21     func(b); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
22     func(c); // ok
23     func(d); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
24     func(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
25     func(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
26 }
27 
28 #else
29 
30 void test_wrapper(){
31     int a = 1;
32     const int b = 1;
33     int& c = a; 
34     const int& d = a;
35     wrapper(a); // ok
36     wrapper(b); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
37     wrapper(c); // ok
38     wrapper(d); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
39     wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
40     wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
41 }
42 
43 #endif

这个情况,其实编译结果还是不一致的。若想知道具体细节请自己编译一遍。

第4组测试:func参数为 const int& p

 1 void func(const int& p) {
 2 }
 3 
 4 int number99(){
 5     return 99;
 6 }
 7 
 8 template <typename T>
 9 void wrapper(T& p) { func(p); }
10 
11 //used for switching the two test cases below
12 #define TEST_FUNC
13 
14 #if defined(TEST_FUNC)
15 
16 void test_func(){
17     int a = 1;
18     const int b = 1;
19     int& c = a; 
20     const int& d = a;
21     func(a); // ok
22     func(b); // ok
23     func(c); // ok
24     func(d); // ok
25     func(1); // ok
26     func(number99()); // ok
27 }
28 
29 #else
30 
31 void test_wrapper(){
32     int a = 1;
33     const int b = 1;
34     int& c = a; 
35     const int& d = a;
36     wrapper(a); // ok
37     wrapper(b); // ok
38     wrapper(c); // ok
39     wrapper(d); // ok
40     wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
41     wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
42 }
43 
44 #endif

结果和第1、2组一样。

小结

测试先到这里。由测试结果可以知道,这个wrapper是失败的(第1、2、4组测试,连最基本的编译结果都不一样)。

在c++11之前,对上面的不一致问题,是用非常暴力的方式的解决的,方式就是重载出N个wrapper的函数。

比如,把上面的第1组测试的代码改成:

 1 void func(const int p) {
 2 }
 3 
 4 int number99(){
 5     return 99;
 6 }
 7 
 8 template <typename T>
 9 void wrapper(T& p) { func(p); }
10 
11 /*--- a override of wrapper ---*/
12 template <typename T>
13 void wrapper(const T& p) { func(p); }
14 
15 
16 //used for switching the two test cases below
17 #define TEST_FUNC
18 
19 #if defined(TEST_FUNC)
20 
21 void test_func(){
22     int a = 1;
23     const int b = 1;
24     int& c = a; 
25     const int& d = a;
26     func(a); // ok
27     func(b); // ok
28     func(c); // ok
29     func(d); // ok
30     func(1); // ok
31     func(number99()); // ok
32 }
33 
34 #else
35 
36 void test_wrapper(){
37     int a = 1;
38     const int b = 1;
39     int& c = a; 
40     const int& d = a;
41     wrapper(a); // ok
42     wrapper(b); // ok
43     wrapper(c); // ok
44     wrapper(d); // ok
45     wrapper(1); // ok
46     wrapper(number99()); // ok
47 }
48 
49 #endif

增加了这段代码: cpp template <typename T> void wrapper(const T& p) { func(p); } test_wrapper就编译通过了。(只需要注意编译结果的一致性,暂且忽略运行结果的一致性)

由此可以思考一下:如果func有N个参数,每个参数都要写const和非const两个版本,那么总共要写的wrapper函数就有2的n次方个!多么可怕。

reference deduction (collapsing)

引用推导(或引用折叠)规则,是c++11开始才有的一个说法,具体是怎么回事呢?请看下面的代码:

typedef int&  lref;
typedef int&& rref;
int n = 100;
lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

(摘自http://en.cppreference.com/w/cpp/language/reference )

我用visual studio 2015跑了下这段代码:

1.png

看来是没错的。

总结了下这套推导规则:

  • A& & -> A&
  • A& && -> A&
  • A&& & -> A&
  • A&& && -> A&&

(记忆方法:只要有&,结果肯定是&)

C++中,有一对重要的兄弟:lvalue(左值)、rvalue(右值)。如何区分?简单来说就是,具名的是左值,不具名的是右值。

要注意一个事情:上面的4个变量r1、r2、r3、r4都是左值。即使r4的类型是右值引用,但因为r4是具名的,所以r4是左值。

Perfect forwarding

什么是完美转发?说白了就是要把上面那个不完美的wrapper,改造成完美的wrapper。

而且,改造过程只能在c++11以上版本才能实现。幸运的是,实现方式并不复杂,如下:

1 template <typename T>
2 void wrapper(T&& p) { 
3     func(std::forward<T>(p));
4  }

再做这个新wrapperd的测试前,先把一些相关的函数介绍一遍。

remove_reference

vs2015给出的remove_reference实现是:

 1 template<class _Ty>
 2  struct remove_reference
 3  {    // remove reference
 4  typedef _Ty type;
 5  };
 6 
 7 template<class _Ty>
 8  struct remove_reference<_Ty&>
 9  {    // remove reference
10  typedef _Ty type;
11  };
12 
13 template<class _Ty>
14  struct remove_reference<_Ty&&>
15  {    // remove rvalue reference
16  typedef _Ty type;
17  };

这个东西,其实一目了然了,用3个重载保证remove_reference<类型>::type肯定没有&符号。后面的2个重载是必须的,当只定义了不带&符号的remove_reference时,remove_reference会没有效果。

其中比较诡异的是,后面的2个同名是不能单独存在的(会编译报错),必须先定义不带&符号的remove_reference,才能定义带&符号的remove_reference。(可以自己编译试试)

remove_reference在std::forward里会被使用。

forward函数

wrapper用到的std::forward,vs2015给出的实现是:

 1     // TEMPLATE FUNCTION forward
 2 template<class _Ty> inline
 3     _CONST_FUN _Ty&& forward(
 4         typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT
 5     {    // forward an lvalue as either an lvalue or an rvalue
 6     return (static_cast<_Ty&&>(_Arg));
 7     }
 8 
 9 template<class _Ty> inline
10     _CONST_FUN _Ty&& forward(
11         typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
12     {    // forward an rvalue as an rvalue
13     static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
14     return (static_cast<_Ty&&>(_Arg));
15     }

看着有点复杂,改造下(只保留关键代码):

 1 template<class T> 
 2 T&& Forward(
 3     typename remove_reference<T>::type& p)
 4 {    
 5     // 把一个左值转发成左值或右值
 6     return (static_cast<T&&>(p));
 7 }
 8 
 9 template<class T> 
10 T&& Forward(
11     typename remove_reference<T>::type&& p)
12 {    
13     // 把一个右值转发成右值
14     return (static_cast<T&&>(p));
15 }

测试一下这个函数的运行情况:

 1 //姑且称这个Forward为左值Forward
 2 template<class T>
 3 T&& Forward(
 4     typename remove_reference<T>::type& p)
 5 {
 6     // 把一个左值转发成左值或右值
 7     return (static_cast<T&&>(p));
 8 }
 9 
10 //右值Forward
11 template<class T>
12 T&& Forward(
13     typename remove_reference<T>::type&& p)
14 {
15     // 把一个右值转发成右值
16     return (static_cast<T&&>(p));
17 }
18 
19 int main() {
20 
21  int a = 10;
22  int& b = a;
23  int&& c = 10;
24  Forward<int>(a);
25  Forward<int&>(a);
26  Forward<int&&>(a);
27 
28  Forward<int>(b);
29  Forward<int&>(b);
30  Forward<int&&>(b);
31 
32  Forward<int>(c);
33  Forward<int&>(c);
34  Forward<int&&>(c);
35 
36  Forward<int>(10);
37  Forward<int&>(10);
38  Forward<int&&>(10);
39 
40  return 0;
41 }

断点调试发现:

  • 以左值(a、b、c具名,所以是左值)作为参数去调用三个实例化模板函数,进入的都是左值Forward
  • 以右值(10不具名,所以是右值)作为参数去调用三个实例化模板函数,进入的都是右值Forward
  • 在左值Forward函数体内,p的类型是int&
  • 在右值Forward函数体内,p的类型是int&&

左值Forward的推导过程

Forward(a),T是int,所以:

int && Forward(typename remove_reference<int>::type& p)
{
    return (static_cast<int &&>(p));
}

最终变成:

int & Forward(int& p)
{
    return (static_cast<int &>(p));
}

Forward(a),T是int&,所以:

int & && Forward(typename remove_reference<int &>::type& p)
{
    return (static_cast<int & &&>(p));
}

根据上文说的引用推导规则,这个函数会变成:

int & Forward(int& p)
{
    return (static_cast<int &>(p));
}

Forward(a),T是int&&,所以:

int && && Forward(typename remove_reference<int &&>::type& p)
{
    return (static_cast<int && &&>(p));
}

根据上文说的引用推导规则,这个函数会变成:

int && Forward(int& p)
{
    return (static_cast<int &&>(p));
}

小结:

  • 当Forward的参数是左值时,调用的是左值Forward版本
  • 当Forward的‘模板类型’是int或int&时,Forward实例化成:
int & Forward(int& p)
{
    return (static_cast<int &>(p));
}
  • 当Forward的‘模板类型’是int&&时,Forward实例化成:
int && Forward(int& p)
{
    return (static_cast<int &&>(p));
}

也就是说,

  • 当参数是左值时,它必然是以int&(即左值引用)的形式进入到Forward;
  • 当Forward模板类型是int或int&时,返回值类型必然是int&;
  • 当Forward模板类型是int&&时,返回值类型必然是int&&。

右值Forward的推导过程

Forward(10),T是int,所以:

int && Forward(typename remove_reference<int>::type&& p)
{
    return (static_cast<int &&>(p));
}

最终变成:

int && Forward(int && p)
{
    return (static_cast<int &&>(p));
}

Forward(10),T是int&,所以:

int & && Forward(typename remove_reference<int &>::type&& p)
{
    return (static_cast<int & &&>(p));
}

根据上文说的引用推导规则,这个函数会变成:

int & Forward(int && p)
{
    return (static_cast<int &>(p));
}

Forward(10),T是int&&,所以:

int && && Forward(typename remove_reference<int &&>::type&& p)
{
    return (static_cast<int && &&>(p));
}

根据上文说的引用推导规则,这个函数会变成:

int && Forward(int && p)
{
    return (static_cast<int &&>(p));
}

小结:

  • 当Forward的参数是右值时,调用的是右值Forward版本
  • 当Forward的‘模板类型’是int或int&&时,Forward实例化成:
int && Forward(int && p)
{
    return (static_cast<int &&>(p));
}
  • 当Forward的‘模板类型’是int&时,Forward实例化成:
int & Forward(int && p)
{
    return (static_cast<int &>(p));
}
  • 也就是说,当参数是右值时,它必然是以int&&(即右值引用)的形式进入到Forward;
  • 当Forward模板类型是int或int&&时,返回值类型必然是int&&;
  • 当Forward模板类型是int&时,返回值类型必然是int&。

universal references

对完美wrapper的另一个部分做分析:

1 template <typename T>
2 void wrapper(T&& p) { 
3  }

这个wrapper有什么效果?测试下:

 1 template <typename T>
 2 void wrapper(T&& p) {
 3 }
 4 
 5 int main() {
 6 
 7  int a = 10;
 8  int& b = a;
 9  int&& c = 10;
10  wrapper(a);
11  wrapper(b);
12  wrapper(c);
13  wrapper(10);
14 
15  return 0;
16 }

用vs2015断点进入wrapper函数,发现:

  • wrapper(a),p的类型是int&
  • wrapper(b),p的类型是int&
  • wrapper(c),p的类型是int&
  • wrapper(10),p的类型是int&&

这个规则有点不直观。wrapper的模板类型是T,参数是T&&,为什么传一个左值int a进去,不是得到int && p,而是int & p?

这是因为,在这个wrapper中,T&& p并不是单纯的右值引用,而是叫universal references。(泛引用?)

在Scott Meyers的这篇文章Universal References in C++11—Scott Meyers中,Scott Meyers做了如下定义:

If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

翻译一下:

如果一个变量或参数被声明为推导类型T对应的T&&类型,那么这个变量或参数是一个universal reference。

回到wrapper函数

1 template <typename T>
2 void wrapper(T&& p) { 
3     func(std::forward<T>(p));
4  }

根据之前的测试,可以知道:

(为了解释方便,以int来说明)

  1. 当传递给wrapper的实参是左值时,T&& p变成 int& p
  2. 当传递给wrapper的实参是右值时,T&& p变成 int&& p
  3. wrapper的forward是左值forward (因p具名,p是左值)
  4. p是以int&的形式进入到forward (p是左值)
  5. 当forward模板类型是int或int&时,forward返回值类型必然是int&
  6. 当forward模板类型是int&&时,forward返回值类型必然是int&&
  7. 根据1、2、5、6可以得出8、9
  8. 当传递给wrapper的实参是左值时,T&& p变成 int& p,forward返回值类型必然是int&
  9. 当传递给wrapper的实参是右值时,T&& p变成 int&& p,forward返回值类型必然是int&&

所谓的完美转发,指的就是第8、9这两个性质。

调用层给wrapper任意类型(int a、int& a、int&& a)的左值,wrapper都以int&转发给func; 调用层给wrapper任意右值(不具名常量、函数返回值等),wrapper都以int&&转发给func。

最后,再来看下一开始的不完美wrapper的问题:

 1 void func(const int p) {
 2 }
 3 
 4 int number99(){
 5     return 99;
 6 }
 7 
 8 template <typename T>
 9 void wrapper(T& p) { func(p); }
10 
11 //used for switching the two test cases below
12 #define TEST_FUNC
13 
14 #if defined(TEST_FUNC)
15 
16 void test_func(){
17     int a = 1;
18     const int b = 1;
19     int& c = a; 
20     const int& d = a;
21     func(a); // ok
22     func(b); // ok
23     func(c); // ok
24     func(d); // ok
25     func(1); // ok
26     func(number99()); // ok
27 }
28 
29 #else
30 
31 void test_wrapper(){
32     int a = 1;
33     const int b = 1;
34     int& c = a; 
35     const int& d = a;
36     wrapper(a); // ok
37     wrapper(b); // ok
38     wrapper(c); // ok
39     wrapper(d); // ok
40     wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
41     wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
42 }
43 
44 #endif

wrapper(1) 和 wrapper(number99()) 这2个为何报错,读者现在应该明白了。

好吧我还是把话说完吧:

wrapper的T& p遇到任何引用类型的T,都只会变成int& p。因为引用折叠规则(见上文)就是这样子规定。而又因为1和number99()返回值,都是右值,那么传递进wrapper时,就是int& p = 1, int& p = number99(),显然这会编译错误。

本文结束。

参考资料

http://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c/

(未经授权禁止转载)
Written on November 2, 2015

博主将十分感谢对本文章的任意金额的打赏^_^