对象拷贝时的编译器优化
一、简介
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返
回值的过程中可以省略的拷贝
- 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新⼀点的编
译器对于连续⼀个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行
跨行跨表达式的合并优化
二、测试代码
先给出完整测试代码,读者可以先自己运行思考
//对象拷贝时的编译器优化
#include<iostream>
using namespace std;
class A {
public:
A(int a = 0):_a1(a) {
cout << "调用了构造函数" << endl;
}
A(const A& aa):_a1(aa._a1) {
cout << "调用了拷贝构造函数" << endl;
}
A& operator=(const A& aa) {
cout << "调用了赋值运算符重载" << endl;
if (this != &aa) {
_a1 = aa._a1;
}
return *this;
}
~A() {
cout << "调用了析构函数" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa) {}
A f2() {
A aa;
return aa;
}
int main() {
//1.传值传参
//1.1传有名对象,构造+拷贝构造,不会优化
A aa1;
f1(aa1);
cout << endl;
//1.2传匿名对象,构造+拷贝构造->优化为直接构造
f1(A());
cout << endl;
cout << "***********************************************" << endl << endl;
//2.隐式类型转换,构造+拷贝构造->优化为直接构造
f1(1);
cout << endl;
cout << "***********************************************" << endl << endl;
//3.传值返回
//3.1不接收返回值
//不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值
//无优化 (vs2019 debug)
//一些编译器会优化得更厉害,将构造的局部对象和拷贝构造的临时对象优化为直接构造(vs2022 debug和vs2019 release)
f2();
cout << endl;
//3.2接收返回值,用于创建对象
//返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
//一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造。(vs2022 debug和vs2019 release)
A aa2 = f2();
cout << endl;
//本质只创建了aa2,底层让aa变成aa2的引用,这样就不用拷贝了
//3.3接收返回值,用于赋值给已创建的对象
//一个表达式中,开始构造,中间拷贝构造+赋值重载->无法优化(vs2019 debug)
//一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝临时对象合并为一个直接构造(vs2022 debug)
aa1 = f2();
cout << endl;
cout << "***********************************************" << endl << endl;
return 0;
}
这里只演示vs2022 debug下的运行结果
调用了构造函数
调用了拷贝构造函数
调用了析构函数
调用了构造函数
调用了析构函数
***********************************************
调用了构造函数
调用了析构函数
***********************************************
调用了构造函数
调用了析构函数
调用了构造函数
调用了构造函数
调用了赋值运算符重载
调用了析构函数
***********************************************
调用了析构函数
调用了析构函数
三、代码讲解
情况1:函数传值调用
//1.1传有名对象,构造+拷贝构造,不会优化
A aa1;
f1(aa1);
cout << endl;
//1.2传匿名对象,只有构造,不需要拷贝构造(这里没有优化,只是演示匿名对象相比有名对象的优势
f1(A());
1.1 传有名对象
创建aa1时调用一次构造函数
传值调用时将aa1赋值给f1的形参aa,调用一次拷贝构造函数
出函数作用域调用一次aa的析构函数
运行结果为
调用了构造函数 调用了拷贝构造函数 调用了析构函数
1.2 传匿名对象
构造+拷贝构造->优化为直接构造
出函数作用域调用一次aa的析构函数
调用了构造函数 调用了析构函数
情况2:隐式类型转换
//2.隐式类型转换,构造+拷贝构造->优化为直接构造
f1(1);
cout << endl;
如果不优化:
将int类型的1转换为A类类型的1,调用一次构造函数
传值调用时将A类类型的1赋值给f1的形参aa,调用一次拷贝构造函数
出函数作用域调用一次aa的析构函数
则运行结果应为
调用了构造函数 调用了拷贝构造函数 调用了析构函数
优化后:
构造+拷贝构造->优化为直接构造
出函数作用域调用一次aa的析构函数
调用了构造函数 调用了析构函数
情况3:传值返回
3.1 不接收返回值
//3.1不接收返回值
//不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值
//无优化 (vs2019 debug)
//一些编译器会优化得更厉害,将构造的局部对象和拷贝构造的临时对象优化为直接构造(vs2022 debug和vs2019 release)
f2();
如果不优化:
创建aa时调用一次构造函数
传值返回时要创建一个临时对象,调用一次拷贝构造函数
出函数作用域先调用一次aa的析构函数,再调用一次临时对象的析构函数
则运行结果应为
调用了构造函数 调用了拷贝构造函数 调用了析构函数 调用了构造函数
优化后:
检测到没有传递返回值,不创建临时对象
只需创建aa时调用一次构造函数,出函数作用域调用一次aa的析构函数
调用了构造函数 调用了析构函数
3.2 接收返回值,用于创建对象
//3.2接受返回值
//返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019 debug)
//一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造。(vs2022 debug和vs2019 release)
A aa2 = f2();
cout << endl;
//本质只创建了aa2,底层让aa变成aa2的引用,这样就不用拷贝了
如果不优化:
创建aa时调用一次构造函数
传值返回时要创建一个临时对象,调用一次拷贝构造函数
出函数作用域调用一次aa的析构函数
函数返回值(临时对象)传给aa2时还要调用一次拷贝构造函数
临时对象销毁,再调用一次临时对象的析构函数
最后程序结束,aa2销毁,调用一次aa2的析构函数
则运行结果应为
调用了构造函数 调用了拷贝构造函数 调用了析构函数 调用了拷贝构造函数 调用了析构函数 调用了析构函数
优化后:
vs2019 debug省略了一次拷贝构造和析构调用了构造函数 调用了拷贝构造函数 调用了析构函数 调用了析构函数
vs2022 debug省略了两次拷贝构造和析构
调用了构造函数 调用了析构函数
本质:只构造了aa2,aa是aa2的引用,可以打印aa2和aa的地址,发现地址一样
3.3 接收返回值,用于赋值给已创建的对象
//3.3接收返回值,用于赋值给已创建的对象
//一个表达式中,开始构造,中间拷贝构造+赋值重载->无法优化(vs2019 debug)
//一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝临时对象合并为一个直接构造(vs2022 debug)
aa1 = f2();
cout << endl;
如果不优化:
创建aa时调用一次构造函数
传值返回时要创建一个临时对象,调用一次拷贝构造函数
出函数作用域调用一次aa的析构函数
函数返回值(临时对象)赋值给aa1时调用一次赋值运算符重载
临时对象销毁,再调用一次临时对象的析构函数
最后程序结束,aa1销毁,调用一次aa1的析构函数
则运行结果应为
调用了构造函数 调用了拷贝构造函数 调用了析构函数 调用了赋值运算符重载 调用了析构函数 调用了析构函数
优化后:
省略了拷贝构造和析构
调用了构造函数 调用了赋值运算符重载 调用了析构函数 调用了析构函数
四、总结
-
编译器优化的前提是不影响正确性,只有确保不会影响正确性的前提下编译器才考虑去优化
-
传值传参尽量不要用有名对象,而是使用匿名对象或者隐式类型转换
-
传值返回尽量不要使用赋值运算符重载,而是使用拷贝构造函数