C++返回值优化与移动语义的思考
C++的class本质上是值语义的,它在作为函数返回值时可能会造成不必要的拷贝
返回值优化(RVO)
返回值优化是编译器对返回一个值语义对象时进行的优化,这有助于性能优化
假如我们有一个Foo类,它的定义如下所示
class Foo {
public:
Foo() {
cout << "construct()\n";
}
Foo(const Foo&) {
cout << "copy()\n";
}
~Foo() {
cout << "destruct()\n";
}
};
我们返回一个Foo对象
Foo f() {
Foo foo;
return foo;
}
int main() {
Foo foo = f();
}
它的输出如下:
construct()
destruct()
可以看出,main函数中的两个对象没有经过拷贝构造
不过返回值优化仅在返回值对象确定的情况下才会使用,我们来看下面一个例子
Foo f(int n) {
Foo foo1, foo2;
if (n % 2) {
return foo1;
} else {
return foo2;
}
}
int main() {
Foo foo = f(3);
}
它的输出结果如下:
construct()
construct()
copy()
destruct()
destruct()
destruct()
可以很容易看出来,这次并没有返回值优化,原因就在于编译器不清楚main函数中的foo所占有的栈空间该与哪个if/else分支的对象对应
所以在这种情况下,我们要避免这种函数的编写
移动语义
C++11的移动语义解决了上述的部分问题,比如像std::string,std::vector这种容器保存了它所占有的资源的指针,它本身的空间占用并不大(当然std::string具体实现可能有SSO优化,不过所占用的空间也是很小的
我们为Foo类添加移动构造函数
class Foo {
public:
Foo() {
cout << "construct()\n";
}
Foo(const Foo&) {
cout << "copy()\n";
}
Foo(Foo&&) {
cout << "move()\n";
}
~Foo() {
cout << "destruct()\n";
}
};
再运行一次
输出结果:
construct()
construct()
move()
destruct()
destruct()
destruct()
我们可以发现move操作替换了copy,编译器在返回值时会首先匹配移动构造函数
这在返回std::vector,std::string之类的对象时性能提升是很大的,原因就是移动时仅转移了它们内部的资源指针。
那么我们有必要显式添加std::move(局部对象)让编译器完成移动操作呢?
运行下面代码
Foo f() {
Foo foo;
return std::move(foo);
}
int main() {
Foo foo = f();
}
输出结果:
construct()
move()
destruct()
destruct()
我们从结果可推知,返回时显式对局部对象添加std::move(),会阻止编译器进行返回值优化,这样可能会造成不必要的性能损失
所以我们最好在返回一个非局部对象时才考虑使用std::move()来移动它
对C++编程的启示
由于值语义的存在,设计函数时返回一个对象对于C++程序员是有一定的心理负担的,而现代C++提供了返回值优化与移动语义来减轻程序员的负担
不过对于比较复杂的函数或者sizeof(class)比较大的情况,传递引用或者指针还是一种比较好的方式