C++对象在作为函数参数的拷贝研究
介绍
C++对象在作为函数参数以及返回值的copy策略一直是我困扰的一个问题,特别是在今天看到了C++11的新增加的特性std::move以及右值概念之后,激发了我的求知欲,决定把这一块详细的搞清楚。
测试对象代码
写了个简单的测试对象,在每个不同的构造函数以及析构函数中打印log
1 | class A |
测试传参
测试代码
1 | // 参数传值; |
测试结果
经过测试,Visual Studio 2017 Debug模式,Visual Studio 2017 Release 模式,g++ 4.8 -g -O0模式,g++ 4.8 -O3模式都输出如下内容
1 | ====== test function argument object passed by value |
总结
可以看到无论何种情况,都应该使用引用的方式来传入对象值,避免进行不必要的拷贝对象的消耗。至于是否需要加const作为引用的修饰,建议是如果该对象需要存储函数的操作结果的,就不加const,其他情况都使用const引用来传参数。
延伸
std::bind与std::function是C++11提供的一个高级功能,使用这两个对象可以写出非常简单的函数回调操作,那它内部是如何处理对象作为参数这一操作的呢?
首先测试最简单的函数调用代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int 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测试结果跟直接使用原生函数一样。传引用的方式会优于传值。
使用function对象存储bind对象的内容,使用的还是最简单的函数调用代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int 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
destructg++ 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开销
- 传引用的结果与调用原生函数一样
至于为什么会出现这种情况,还需要后续继续研究,但结果可以看出传引用的方式会优于传值。
使用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
40int 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,传引用的方式都会优于传值。
使用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
40int 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 |
|