最近在学习《effective modern C++》,到条款二十五,看到了C++返回值拷贝的说明,故做一下总结。并参考了网上的资料深入理解C++中的RVO。
概述
我将以下面的类为例子,说明C++在RVO的情况下以及关闭RVO情况下,其函数返回值是如何返回给调用者的,并给出其汇编代码的说明。环境是Ubuntu Linux,编译器为GCC7.5.0。
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(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 &operator=(const Obj &&obj) { // 移动赋值构造函数
std::cout << "in operator=(const Obj &&obj)" << std::endl;
return *this;
}
~Obj() { // 析构函数
std::cout << "in ~Obj() " << this << std::endl;
}
private:
int n;
};
关闭RVO
编译命令加上-fno-elide-constructors
表示关闭RVO 优化开关。定义函数fun1
,并调用。
//test28_RVO.cpp
Obj fun1() {
Obj obj;
// do sth;
return obj;
}
int main() {
Obj a = fun1();
return 0;
}
编译:g++ -fno-elide-constructors -g -o test28_RVO test28_RVO.cpp
。
结果打印:如下图所示。
- 定义拷贝构造函数以及移动构造函数的情况下:
- 定义拷贝构造函数而未定义移动构造函数的情况下:
结果分析:从打印可以分析出,在关闭RVO优化的情况下,C++返回一个对象会调用拷贝构造函数(或者移动构造函数)2次。分析其汇编代码,可知,在main
函数调用函数fun1()
的时候,调用者会将一个栈的地址传递给fun1()
。在函数fun1()
内,当调用构造函数创建obj
之后,会将obj
拷贝到这个栈的地址,作为一个临时对象。函数fun()
调用结束之后,会再调用一次拷贝构造函数,临时对象拷贝到接收者即a
中。
在RVO下
代码和上面的相同。
//test28_RVO.cpp
Obj fun1() {
Obj obj;
// do sth;
return obj;
}
int main() {
Obj a = fun1();
return 0;
}
编译:g++ -g -o test28_RVO test28_RVO.cpp
。
结果打印:如下图所示。
结果分析:在启用RVO优化的情况下,只调用了一次构造函数,拷贝构造函数没有被调用。
从汇编代码结果可以看出,main
函数将地址-0x1c(%rbp)
传递给函数fun1()
,之后,fun1()
在调用Obj
构造函数的时候,直接在这个地址上进行构造,免去了临时对象的创建以及拷贝。
RVO的条件
《modern effective c++》中指出,在开启RVO之后,返回局部对象只有在满足以下两个条件的情况下,编译器才会进行RVO优化。 (1)局部对象与函数返回值的类型相同。 (2)局部对象就是要返回的东西。函数形参不满足要求。 注意:当函数不同控制路径返回不同局部变量时,不会进行拷贝消除的操作,因为其不知道要返回哪个对象。
返回值为std::move的情况
代码如下:
Obj fun2() {
Obj obj;
// do sth;
return std::move(obj);
}
int main() {
Obj a = fun2();
return 0;
}
编译:g++ -g -o test28_RVO test28_RVO.cpp
。
结果打印:如下图所示
- 定义拷贝构造函数以及移动构造函数的情况下:
- 定义拷贝构造函数,未移动构造函数的情况下(之所以编译器未报错,是因为const修饰的左值形参能够接收一个右值实参):
结果分析:从汇编代码中可以看出,main将变量a
的地址传递给fun2()
。fun2()
创建Obj obj
,然后调用移动构造函数将obj
移动到a
中。