关于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <vector>
using namespace std;


class Item {
public:
    char name;
    int val;
public:
    ~Item() {
        printf("[dtor called] (%c, %i)\n", name, val);
    }
    Item() :name('_'), val(0) {
        printf("[default ctor called] \n");
    }

    Item(char n, int v) :name(n), val(v) {
        printf("[ctor called] (%c, %i)\n", name, val);
    }
    Item(Item&& a) {
        printf("[move ctor called] (%c, %i)\n", a.name, a.val);
        name = a.name;
        val = a.val;
    }
};


int main() {
    vector<Item> v1;
    for (int i = 0; i < 3; i++) {
        v1.emplace_back('a', i);
    }
    printf("-----------------------\n");
    vector<Item> v2;
    v2.reserve(10);
    for (int i = 0; i < 3; i++) {
        v2.emplace_back('b', i);
    }
    printf("-----------------------\n");
    vector<Item> v3;
    v3.push_back({ 'c', 3 });
    printf("-----------------------\n");
    return 0;
}

编译:

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

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[ctor called] (a, 0)
[move ctor called] (a, 0)
[dtor called] (a, 0)
[ctor called] (a, 1)
[move ctor called] (a, 0)
[move ctor called] (a, 1)
[dtor called] (a, 0)
[dtor called] (a, 1)
[ctor called] (a, 2)
-----------------------
[ctor called] (b, 0)
[ctor called] (b, 1)
[ctor called] (b, 2)
-----------------------
[ctor called] (c, 3)
[move ctor called] (c, 3)
[dtor called] (c, 3)
-----------------------
[dtor called] (c, 3)
[dtor called] (b, 0)
[dtor called] (b, 1)
[dtor called] (b, 2)
[dtor called] (a, 0)
[dtor called] (a, 1)
[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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void func(const int p) {
}

int number99(){
    return 99;
}

template <typename T>
void wrapper(T& p) { func(p); }

//used for switching the two test cases below
#define TEST_FUNC

#if defined(TEST_FUNC)

void test_func(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    func(a); // ok
    func(b); // ok
    func(c); // ok
    func(d); // ok
    func(1); // ok
    func(number99()); // ok
}

#else

void test_wrapper(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    wrapper(a); // ok
    wrapper(b); // ok
    wrapper(c); // ok
    wrapper(d); // ok
    wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}

#endif

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void func(int p) {
}

int number99(){
    return 99;
}

template <typename T>
void wrapper(T& p) { func(p); }

//used for switching the two test cases below
#define TEST_FUNC

#if defined(TEST_FUNC)

void test_func(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    func(a); // ok
    func(b); // ok
    func(c); // ok
    func(d); // ok
    func(1); // ok
    func(number99()); // ok
}

#else

void test_wrapper(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    wrapper(a); // ok
    wrapper(b); // ok
    wrapper(c); // ok
    wrapper(d); // ok
    wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}

#endif

结果和第1组一样。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void func(int& p) {
}

int number99(){
    return 99;
}

template <typename T>
void wrapper(T& p) { func(p); }

#define TEST_FUNC

#if defined(TEST_FUNC)

void test_func(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    func(a); // ok
    func(b); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
    func(c); // ok
    func(d); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
    func(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    func(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}

#else

void test_wrapper(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    wrapper(a); // ok
    wrapper(b); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
    wrapper(c); // ok
    wrapper(d); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
    wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}

#endif

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void func(const int& p) {
}

int number99(){
    return 99;
}

template <typename T>
void wrapper(T& p) { func(p); }

//used for switching the two test cases below
#define TEST_FUNC

#if defined(TEST_FUNC)

void test_func(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    func(a); // ok
    func(b); // ok
    func(c); // ok
    func(d); // ok
    func(1); // ok
    func(number99()); // ok
}

#else

void test_wrapper(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    wrapper(a); // ok
    wrapper(b); // ok
    wrapper(c); // ok
    wrapper(d); // ok
    wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}

#endif

结果和第1、2组一样。

小结

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void func(const int p) {
}

int number99(){
    return 99;
}

template <typename T>
void wrapper(T& p) { func(p); }

/*--- a override of wrapper ---*/
template <typename T>
void wrapper(const T& p) { func(p); }


//used for switching the two test cases below
#define TEST_FUNC

#if defined(TEST_FUNC)

void test_func(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    func(a); // ok
    func(b); // ok
    func(c); // ok
    func(d); // ok
    func(1); // ok
    func(number99()); // ok
}

#else

void test_wrapper(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    wrapper(a); // ok
    wrapper(b); // ok
    wrapper(c); // ok
    wrapper(d); // ok
    wrapper(1); // ok
    wrapper(number99()); // ok
}

#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
2
3
4
template <typename T>
void wrapper(T&& p) { 
    func(std::forward<T>(p));
 }

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

remove_reference

vs2015给出的remove_reference实现是:

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

template<class _Ty>
    struct remove_reference<_Ty&>
    {    // remove reference
    typedef _Ty type;
    };

template<class _Ty>
    struct remove_reference<_Ty&&>
    {    // remove rvalue reference
    typedef _Ty type;
    };

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

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

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

forward函数

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

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

template<class _Ty> inline
    _CONST_FUN _Ty&& forward(
        typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
    {    // forward an rvalue as an rvalue
    static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
    return (static_cast<_Ty&&>(_Arg));
    }

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

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

template<class T> 
T&& Forward(
    typename remove_reference<T>::type&& p)
{    
    // 把一个右值转发成右值
    return (static_cast<T&&>(p));
}

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

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

//右值Forward
template<class T>
T&& Forward(
    typename remove_reference<T>::type&& p)
{
    // 把一个右值转发成右值
    return (static_cast<T&&>(p));
}

int main() {

    int a = 10;
    int& b = a;
    int&& c = 10;
    Forward<int>(a);
    Forward<int&>(a);
    Forward<int&&>(a);

    Forward<int>(b);
    Forward<int&>(b);
    Forward<int&&>(b);

    Forward<int>(c);
    Forward<int&>(c);
    Forward<int&&>(c);

    Forward<int>(10);
    Forward<int&>(10);
    Forward<int&&>(10);

    return 0;
}

断点调试发现:

  • 以左值(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
2
3
template <typename T>
void wrapper(T&& p) { 
 }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
void wrapper(T&& p) {
}

int main() {

    int a = 10;
    int& b = a;
    int&& c = 10;
    wrapper(a);
    wrapper(b);
    wrapper(c);
    wrapper(10);

    return 0;
}

用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
2
3
4
template <typename T>
void wrapper(T&& p) { 
    func(std::forward<T>(p));
 }

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

(为了解释方便,以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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void func(const int p) {
}

int number99(){
    return 99;
}

template <typename T>
void wrapper(T& p) { func(p); }

//used for switching the two test cases below
#define TEST_FUNC

#if defined(TEST_FUNC)

void test_func(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    func(a); // ok
    func(b); // ok
    func(c); // ok
    func(d); // ok
    func(1); // ok
    func(number99()); // ok
}

#else

void test_wrapper(){
    int a = 1;
    const int b = 1;
    int& c = a; 
    const int& d = a;
    wrapper(a); // ok
    wrapper(b); // ok
    wrapper(c); // ok
    wrapper(d); // ok
    wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
    wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}

#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

写作不易,您的支持是我写作的动力!