C++对象在作为函数参数的拷贝研究

介绍

C++对象在作为函数参数以及返回值的copy策略一直是我困扰的一个问题,特别是在今天看到了C++11的新增加的特性std::move以及右值概念之后,激发了我的求知欲,决定把这一块详细的搞清楚。

测试对象代码

写了个简单的测试对象,在每个不同的构造函数以及析构函数中打印log

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
class A
{
public:
A()
{
cout << "construct" << endl;
}

A(const A& a)
{
member = a.member;
cout << "copy construct" << endl;
}

A(A&& a)
{
member = std::move(a.member);
cout << "rvalue copy construct" << endl;
}

~A()
{
cout << "destruct" << endl;
}

public:
string member;
};

测试传参

测试代码

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
// 参数传值;
void argumentObjectPassByValue(A a)
{

}

// 参数传引用;
void argumentObjectPassByRef(const A &a)
{

}

int main()
{
{
// 测试传值;
cout << "====== test function argument object passed by value" << endl;
A a;
argumentObjectPassByValue(a);
}

cout << endl;

{
// 测试传引用;
cout << "====== test function argument object passed by reference" << endl;
A a;
argumentObjectPassByRef(a);
}
}

测试结果

经过测试,Visual Studio 2017 Debug模式,Visual Studio 2017 Release 模式,g++ 4.8 -g -O0模式,g++ 4.8 -O3模式都输出如下内容

1
2
3
4
5
6
7
8
9
====== test function argument object passed by value
construct
copy construct
destruct
destruct

====== test function argument object passed by reference
construct
destruct

总结

可以看到无论何种情况,都应该使用引用的方式来传入对象值,避免进行不必要的拷贝对象的消耗。至于是否需要加const作为引用的修饰,建议是如果该对象需要存储函数的操作结果的,就不加const,其他情况都使用const引用来传参数。

延伸

std::bind与std::function是C++11提供的一个高级功能,使用这两个对象可以写出非常简单的函数回调操作,那它内部是如何处理对象作为参数这一操作的呢?

  1. 首先测试最简单的函数调用代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int main()
    {
    {
    // 测试使用bind对象,并直接传值;
    cout << "====== test use bind object, passed by value" << endl;
    A a;
    auto f = std::bind(argumentObjectPassedByValue, std::placeholders::_1);
    f(a);
    }

    cout << endl;

    {
    // 测试使用bind对象,并直接传引用;
    cout << "====== test use bind object, passed by reference" << endl;
    A a;
    auto f = std::bind(argumentObjectPassedByRef, std::placeholders::_1);
    f(a);
    }
    }

    经过测试,Visual Studio 2017 Debug模式,Visual Studio 2017 Release 模式,g++ 4.8 -g -O0模式,g++ 4.8 -O3模式都输出如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    ====== test use bind object, passed by value
    construct
    copy construct
    destruct
    destruct
    ====== test use bind object, passed by reference
    construct
    destruct

    测试结果跟直接使用原生函数一样。传引用的方式会优于传值。

  2. 使用function对象存储bind对象的内容,使用的还是最简单的函数调用代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int main()
    {
    {
    // 测试使用function存储bind对象,并直接传值;
    cout << "====== test use function hold bind object, passed by value" << endl;
    A a;
    function<void(A)> f = std::bind(argumentObjectPassedByValue, std::placeholders::_1);
    f(a);
    }

    cout << endl;

    {
    // 测试使用function存储bind对象,并直接传引用;
    cout << "====== test use function hold bind object, passed by reference" << endl;
    A a;
    function<void(A)> f = std::bind(argumentObjectPassedByRef, std::placeholders::_1);
    f(a);
    }
    }

    经过测试,Visual Studio 2017 Debug模式,Visual Studio 2017 Release 模式输出如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ====== test use function hold bind object, passed by value
    construct
    copy construct
    rvalue copy construct
    destruct
    destruct
    destruct

    ====== test use function hold bind object, passed by reference
    construct
    destruct

    g++ 4.8 -g -O0模式,g++ 4.8 -O3模式都输出如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ====== test use function hold bind object, passed by value
    construct
    copy construct
    rvalue copy construct
    rvalue copy construct
    destruct
    destruct
    destruct
    destruct

    ====== test use function hold bind object, passed by reference
    construct
    destruct

    测试结果:

    • 传值的结果,VS下会有一次move开销,g++下会有两次move开销
    • 传引用的结果与调用原生函数一样
      至于为什么会出现这种情况,还需要后续继续研究,但结果可以看出传引用的方式会优于传值。
  3. 使用bind存储函数的参数,测试代码:

    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
    int main()
    {
    {
    // 测试使用bind对象存储参数值,并直接传值;
    cout << "====== test use bind object hold argument, passed by value" << endl;
    A a;
    auto f = std::bind(argumentObjectPassedByValue, a);
    f();
    }

    cout << endl;

    {
    // 测试使用bind对象存储参数值,并使用std::ref传值,;
    cout << "====== test use bind object hold argument, passed by value use std::ref" << endl;
    A a;
    auto f = std::bind(argumentObjectPassedByValue, std::ref(a));
    f();
    }

    cout << endl;

    {
    // 测试使用bind对象存储参数值,并直接传引用;
    cout << "====== test use bind object hold argument, passed by ref" << endl;
    A a;
    auto f = std::bind(argumentObjectPassedByRef, a);
    f();
    }

    cout << endl;

    {
    // 测试使用bind对象存储参数值,并使用std::ref传引用,;
    cout << "====== test use bind object hold argument, passed by ref by std::Ref" << endl;
    A a;
    auto f = std::bind(argumentObjectPassedByRef, std::ref(a));
    f();
    }
    }

    经过测试,Visual Studio 2017 Debug模式,Visual Studio 2017 Release 模式,g++ 4.8 -g -O0模式,g++ 4.8 -O3模式都输出如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    ====== test use bind object hold argument, passed by value
    construct
    copy construct
    copy construct
    destruct
    destruct
    destruct

    ====== test use bind object hold argument, passed by value use std::ref
    construct
    copy construct
    destruct
    destruct

    ====== test use bind object hold argument, passed by ref
    construct
    copy construct
    destruct
    destruct

    ====== test use bind object hold argument, passed by ref by std::Ref
    construct
    destruct

    测试结果分析:bind内部会存储一份函数参数的拷贝,但是当传递的是std::ref对象时,就可以避免这次拷贝,但必须确保在调用这个回调前,这个参数对象的原副本还存在,不然会导致行为未定义,出现宕机。但不论是否使用std::ref,传引用的方式都会优于传值。

  4. 使用function对象存储bind对象的内容,并存储函数参数

    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
    int main()
    {
    {
    // 测试使用function存储bind对象,并且bind对象存储参数值,并直接传值;
    cout << "====== test use function hold bind object hold argument, passed by value" << endl;
    A a;
    function<void()> f = std::bind(argumentObjectPassedByValue, a);
    f();
    }

    cout << endl;

    {
    // 测试使用function存储bind对象,并且bind对象存储参数值,并使用std::ref传值,;
    cout << "====== test use function hold bind object hold argument, passed by value use std::ref" << endl;
    A a;
    function<void()> f = std::bind(argumentObjectPassedByValue, std::ref(a));
    f();
    }

    cout << endl;

    {
    // 测试使用function存储bind对象,并且bind对象存储参数值,并直接传引用;
    cout << "====== test use function hold bind object hold argument, passed by ref" << endl;
    A a;
    function<void()> f = std::bind(argumentObjectPassedByRef, a);
    f();
    }

    cout << endl;

    {
    // 测试使用function存储bind对象,并且bind对象存储参数值,并使用std::ref传引用,;
    cout << "====== test use function hold bind object hold argument, passed by ref by std::Ref" << endl;
    A a;
    function<void()> f = std::bind(argumentObjectPassedByRef, std::ref(a));
    f();
    }
    }

    经过测试,Visual Studio 2017 Debug模式,Visual Studio 2017 Release 模式,g++ 4.8 -g -O0模式,g++ 4.8 -O3模式都输出如下内容:

    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
    ====== test use function hold bind object hold argument, passed by value
    construct
    copy construct
    rvalue copy construct
    destruct
    copy construct
    destruct
    destruct
    destruct

    ====== test use function hold bind object hold argument, passed by value use std::ref
    construct
    copy construct
    destruct
    destruct

    ====== test use function hold bind object hold argument, passed by ref
    construct
    copy construct
    rvalue copy construct
    destruct
    destruct
    destruct

    ====== test use function hold bind object hold argument, passed by ref by std::Ref
    construct
    destruct

    测试结果分析:这种方式在未使用std::ref的时候,会比第三种测试方案多一次rvalue的构造,以及一次destruct操作,则是因为function和bind内部会存储一份函数参数的拷贝,当bind赋值给function对象的时候,bind内部的参数对象使用了一次move操作转移给function对象,并释放bind内部的函数参数。其他的行为和第三种测试方案一致,所以与第三种方案的结论一样。

总结

** 1. 当函数参数为对象的时候,都应该使用引用的方式来传入。**
** 2. 是否使用const,取决于该参数是否需要作为函数结果返回。**
** 3. 使用std::bind和std::function来生成回调函数的对象的时候,如果需要bind或者function对象存储参数,则优先考虑使用std::ref来传递,因为这种方式可以减少不必要的拷贝**

完整测试代码

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#include <iostream>
#include <functional>
using namespace std;

class A
{
public:
A()
{
cout << "construct" << endl;
}

A(const A& a)
{
member = a.member;
cout << "copy construct" << endl;
}

A(A&& a)
{
member = std::move(a.member);
cout << "rvalue copy construct" << endl;
}

~A()
{
cout << "destruct" << endl;
}

public:
string member;
};

// 参数传值;
void argumentObjectPassedByValue(A a)
{

}

// 参数传引用;
void argumentObjectPassedByRef(const A &a)
{

}

int main()
{
{
// 测试传值;
cout << "====== test function argument object passed by value" << endl;
A a;
argumentObjectPassedByValue(a);
}

cout << endl;

{
// 测试传引用;
cout << "====== test function argument object passed by reference" << endl;
A a;
argumentObjectPassedByRef(a);
}

cout << endl;

//////////////////////////////////////////////////////////////////////////
//

{
// 测试使用bind对象,并直接传值;
cout << "====== test use bind object, passed by value" << endl;
A a;
auto f = std::bind(argumentObjectPassedByValue, std::placeholders::_1);
f(a);
}

cout << endl;

{
// 测试使用bind对象,并直接传引用;
cout << "====== test use bind object, passed by reference" << endl;
A a;
auto f = std::bind(argumentObjectPassedByRef, std::placeholders::_1);
f(a);
}

cout << endl;

//////////////////////////////////////////////////////////////////////////
//

{
// 测试使用function存储bind对象,并直接传值;
cout << "====== test use function hold bind object, passed by value" << endl;
A a;
function<void(A)> f = argumentObjectPassedByValue; // std::bind(argumentObjectPassedByValue, std::placeholders::_1);
f(a);
}

cout << endl;

{
// 测试使用function存储bind对象,并直接传引用;
cout << "====== test use function hold bind object, passed by reference" << endl;
A a;
function<void(const A&)> f = std::bind(argumentObjectPassedByRef, std::placeholders::_1);;
f(a);
}

cout << endl;

//////////////////////////////////////////////////////////////////////////
//

{
// 测试使用bind对象存储参数值,并直接传值;
cout << "====== test use bind object hold argument, passed by value" << endl;
A a;
auto f = std::bind(argumentObjectPassedByValue, a);
f();
}

cout << endl;

{
// 测试使用bind对象存储参数值,并使用std::ref传值,;
cout << "====== test use bind object hold argument, passed by value use std::ref" << endl;
A a;
auto f = std::bind(argumentObjectPassedByValue, std::ref(a));
f();
}

cout << endl;

{
// 测试使用bind对象存储参数值,并直接传引用;
cout << "====== test use bind object hold argument, passed by ref" << endl;
A a;
auto f = std::bind(argumentObjectPassedByRef, a);
f();
}

cout << endl;

{
// 测试使用bind对象存储参数值,并使用std::ref传引用,;
cout << "====== test use bind object hold argument, passed by ref by std::Ref" << endl;
A a;
auto f = std::bind(argumentObjectPassedByRef, std::ref(a));
f();
}

cout << endl;

//////////////////////////////////////////////////////////////////////////
//

{
// 测试使用function存储bind对象,并且bind对象存储参数值,并直接传值;
cout << "====== test use function hold bind object hold argument, passed by value" << endl;
A a;
function<void()> f = std::bind(argumentObjectPassedByValue, a);
f();
}

cout << endl;

{
// 测试使用function存储bind对象,并且bind对象存储参数值,并使用std::ref传值,;
cout << "====== test use function hold bind object hold argument, passed by value use std::ref" << endl;
A a;
function<void()> f = std::bind(argumentObjectPassedByValue, std::ref(a));
f();
}

cout << endl;

{
// 测试使用function存储bind对象,并且bind对象存储参数值,并直接传引用;
cout << "====== test use function hold bind object hold argument, passed by ref" << endl;
A a;
function<void()> f = std::bind(argumentObjectPassedByRef, a);
f();
}

cout << endl;

{
// 测试使用function存储bind对象,并且bind对象存储参数值,并使用std::ref传引用,;
cout << "====== test use function hold bind object hold argument, passed by ref by std::Ref" << endl;
A a;
function<void()> f = std::bind(argumentObjectPassedByRef, std::ref(a));
f();
}

while (1);
}