返回值优化
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 | #include <iostream>
class Obj {
public:
Obj() { // 构造函数
std::cout << "in Obj() "
<< " " << this << std::endl;
}
Obj(int n) {
std::cout << "in Obj(int) "
<< " " << this << std::endl;
}
Obj(const Obj &obj) { // 拷贝构造函数
std::cout << "in Obj(const Obj &obj) " << &obj << " " << this << std::endl;
}
Obj &operator=(const Obj &obj) { // 赋值构造函数
std::cout << "in operator=(const Obj &obj)" << std::endl;
return *this;
}
~Obj() { // 析构函数
std::cout << "in ~Obj() " << this << std::endl;
}
int n;
};
Obj fun() {
// Obj obj;
// do sth;
// return obj;//具名
return Obj();//非具名
}
int main() {
Obj obj = fun();
std::cout << "&obj is " << &obj << std::endl;
return 0;
}
|
没有优化
| Obj fun() {
Obj obj;//具名
return obj;
}
int main() {
Obj obj = fun();
std::cout << "&obj is " << &obj << std::endl;
return 0;
}
|
调用的Obj类成员函数的顺序应该为:
- 调用构造函数,生成对象
- 调用拷贝构造函数,生成临时对象
- 析构第1步生成的对象
- 调用拷贝构造函数,将第2步生成的临时变量拷贝到main()函数中的局部对象obj中
- 调用析构函数,释放第2部生成的临时对象
- 调用析构函数,释放main()函数中的obj局部对象
| g++ -std=c++11 -fno-elide-constructors -g test.cc -o test && ./test
in Obj() 0x7ffee18a9a00 // 在fun()函数中,构造obj对象
in Obj(const Obj &obj) 0x7ffee18a9a00 0x7ffee18a9a40 // 通过拷贝构造创建临时变量(fun()函数定义的obj--->临时对象)
in ~Obj() 0x7ffee18a9a00 // 析构fun()函数中构造的obj对象(fun()函数定义的obj)
in Obj(const Obj &obj) 0x7ffee18a9a40 0x7ffee18a9a30 // 通过拷贝构造函数构建obj(main函数中的)对象(临时对象--->main()函数定义的obj)
in ~Obj() 0x7ffee18a9a40 // 释放临时对象
&obj is 0x7ffee18a9a30
in ~Obj() 0x7ffee18a9a30 // 释放main()函数中定义的obj对象
|
一共调用了一次构造,两次拷贝构造,三次析构
有优化
| g++ -g -std=c++11 test.cc -o test && ./test
in Obj() 0x7ffd6fb15240
&obj is 0x7ffd6fb15240
in ~Obj() 0x7ffd6fb15240
|
一共调用了一次构造一次析构
原理
RVO(Return Value Optimization)返回值优化,或者NRVO,又名具名返回值优化(Named Return Value Optimization),会把原来fun里边的构造行为放到main中进行构造。
| void fun(Obj &_obj) {
Obj obj(1);
_obj.Obj::Obj(obj); // 拷贝构造函数
return;
}
int main() {
Obj obj; // 仅定义不构造
fun(obj);
return 0;
}
|
不能优化的几种情况
根据不同条件返回不同变量
1
2
3
4
5
6
7
8
9
10
11
12
13 | Obj fun(bool flag) {
Obj o1;
Obj o2;
if (flag) {
return o1;
}
return o2;
}
int main() {
Obj obj = fun(true);
return 0;
}
|
返回全局变量
当返回的对象不是在函数内创建的时候,是无法执行返回值优化的。
| Obj g_obj;
Obj fun() {
return g_obj;
}
int main() {
Obj obj = fun();
std::cout << &obj << std::endl;
return 0;
}
|
返回函数参数
与返回全局变量类似,当返回的对象不是在函数内创建的时候,是无法执行返回值优化的。
代码如下:
| Obj fun(Obj obj) {
return obj;
}
int main() {
Obj o;
Obj obj = fun(o);
std::cout << "in main " << &obj << std::endl;
return 0;
}
|
编译并运行之后,输出:
| in Obj() 0x7ffdbb43da00
in Obj(const Obj &obj) 0x7ffdbb43da00 0x7ffdbb43da10
in Obj(const Obj &obj) 0x7ffdbb43da10 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43da10
in main 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43da00
|
返回成员变量
在某些特殊情况下,即使是未具名变量,也不能RVO。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13 | struct Wraper {
Obj obj;
};
Obj fun() {
return Wraper().obj;
}
int main() {
Obj obj = fun();
std::cout << &obj << std::endl;
return 0;
}
|
编译并运行,结果如下:
| in Obj() 0x7ffed7f85290 // 构造Wraper中的obj对象
in Obj(const Obj &obj) 0x7ffed7f85290 0x7ffed7f852c0 // 通过拷贝赋值给main函数中的局部变量
in ~Obj() 0x7ffed7f85290 // 析构Wraper中的obj对象
0x7ffed7f852c0
in ~Obj() 0x7ffed7f852c0 // 析构main中的局部对象
|
使用move
在返回值上调用std::move()进行返回是一种错误的方式。它会尝试强制调用移动构造函数,但这样会导致RVO失效。因为即使没有显示调用std::move(),编译器优化中也会执行move操作。
代码如下:
| Obj fun() {
Obj obj;
return std::move(obj);
}
int main() {
Obj obj = fun();
return 0;
}
|
输出如下:
| in Obj() 0x7ffe7d4d1720
in Obj(const Obj &&obj)//增加了一次
in ~Obj() 0x7ffe7d4d1720
0x7ffe7d4d1750
in ~Obj() 0x7ffe7d4d1750
|
从上面输出可以看出,与不使用std::move()返回相比,使用std::move()返回增加了一次拷贝构造调用和一次析构调用。
return move(x)
大多数时候没有意义,但有的时候会有意义。
没意义的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | #include<iostream>
struct X{
X() { puts("X()"); }
X(const X&) { puts("X(const X&)"); }
X(X&&)noexcept { puts("X(X&&)"); }
~X() { puts("~X()"); }
};
X f(){
X x;
return std::move(x);
}
int main(){
X x = f();
}
|
运行结果
可以说毫无意义,甚至影响NRVO
优化(在当前语境,即使不std::move
,return
x
,重载决议一样会选择到移动构造。这里的x
是隐式可移动实体)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | #include<iostream>
struct X{
X() { puts("X()"); }
X(const X&) { puts("X(const X&)"); }
X(X&&)noexcept { puts("X(X&&)"); }
~X() { puts("~X()"); }
};
X f(){
X x;
return x;
}
int main(){
X x = f();
}
|
运行结果
以上去除了std::move
,编译器得以正常NRVO
优化,优化了移动构造的开销 。
如果禁用了NRVO
,那么运行结果其实和上面的是一样的。 gcc
加上 -fno-elide-constructors
选项即可。
如果以上用法只能说毫无意义,可能影响性能,但是并不会导致什么错误和问题。那么下面我说的就是真正具备各种危害的了。
| X&& f(){
X x;
return std::move(x);
}
|
引用了局部的对象,函数作用域结束,返回的引用是悬垂的,已然ub
了。
有意义的
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 | #include<iostream>
struct X{
X() { puts("X()"); }
X(const X&) { puts("X(const X&)"); }
X(X&&)noexcept { puts("X(X&&)"); }
~X() { puts("~X()"); }
};
struct Test{
X x;//数据成员 可以return move
X f(){
return std::move(x);//数据成员不是隐式可移动实体,如果不std::move,直接return x,重载决议不会选择移动构造。
}
X f2() {
return x;
}
};
int main(){
Test t;
puts("---------");
t.f();
puts("---------");
t.f2();
puts("---------");
}
|
运行结果
| X()
---------
X(X&&)
~X()
---------
X(const X&)
~X()
---------
~X()
|
数据成员不是隐式可移动实体,如果不 std::move
,而是直接 return x
,重载决议不会选择移动构造。
或者至少需要稍微提一下
和
| class Y {
X x
X foo() {
return x
}
}
|
是完全不一样的。