C++中返回对象的情形及RVO
之前有文章介绍过临时对象和返回值优化RVO方面的问题。见此处。
在C++中,返回对象这一点经常被诟病,因为这个地方的效率比较低,需要进行很多的操作,生成一些临时对象,如果对象比较大的会就会比较耗时。但是在编译器实现的时候,经常是对返回对象的情况进行优化,也就是进行返回值优化 。
在g++中,这个是默认已经进行了优化。以前我希望看看到底C++怎么操作的,但是无法看到,就是因为G++进行了默认的返回值优化RVO。今天在晚上发现可以有一中方法来禁止这个RVO,可以参考这儿。
具体来说就是在编译的时候,加上-fno-elide-constructors这个选项,即:
下面是一个示例,来演示C++在返回对象的时候所做的优化。
代码如下:
#include <iomanip>
using namespace std;
int num=1;
class A{
public:
A(){
id=count++;
pre_id=-1;
cout<<setw(2)<<num++<<": A():id="<<id<<" pre_id="<<pre_id<<endl;
}
A(const A& a){
id=count++;
pre_id=a.id;
cout<<setw(2)<<num++<<": A(const A&):id="<<id<<" pre_id="<<pre_id<<endl;
}
~A(){
cout<<setw(2)<<num++<<": ~A():id="<<id<<" pre_id="<<pre_id<<endl;
}
A& operator=(const A& a){
pre_id=a.id;
cout<<setw(2)<<num++<<": =(const A&):id="<<id<<" pre_id="<<pre_id<<endl;
}
private:
static int count;
int id;
int pre_id;
};
int A::count=0;
A f(){
A a;
return a;
}
A g1(A b){
A a=b;
return a;
}
A g2(A b){
A a;
a=b;
return a;
}
int main(){
A B1=f();
A B2=g1(b1);
A B3=g2(b1);
A c1,c2,c3;
c1=f();
c2=g1(c1);
c3=g2(c1);
return 0;
}
为了便于区分每一个对象,采用变量id来记录对象的标号。实现方式是采用了一个静态变量count来记录生成类的个数。
下面是运行结果。左边部分是不采用-fno-elide-constructors这个选项,即采用RVO优化的情形,中间是部分测试代码,右边是采用-fno-elide-constructors这个选项,即不采用RVO的情形,所以右边是我们需要看的,需要分析的类的真正的执行过程。同时为了便于标示比较,对运行的每一行进行了标号:
对右面的运行结果进行解析如下:
先调用函数f(),在函数f中:
A a;
return a;
}
先调用A的默认构造函数A()生成局部对象a:
此时a的id=0,然后因为f的返回值是一个A的对象,此时C++会利用a来调用复制构造函数来生成一个临时对象:
此临时对象的id=1, 同时因为要离开函数f所以需要析构局部对象a:
在主函数main中,对象B1是利用函数f的返回值来进行初始化的:
所以调用复制构造函数来对B1进行初始化:
这样的话B1的id=2,而之前的id=1临时对象因为已经完成了任务,所以C++对其进行了析构:
这样B1对象就构造完毕了。 在这个过程中,对于返回值为对象(非引用或指针的情形)C++所采取的最原始的办法就是先构造一个临时对象来保存返回值,然后再利用这个临时对象来进行操作。当这个临时对象的任务完成之后就将其销毁了。
(2).对于A B2=g1(B1);
先调用函数g1
A a=b;
return a;
}
可以看到g1函数是带参数的,这其中也牵扯到传值参数的问题。由于是按值传递,所以需要进行复制,调用复制构造函数。
所以,首先调用复制构造函数,利用B1来构造g1函数中的形式参数b:
此时形式参数作为一个局部变量,其id=3。接着运行 :
这一句是利用形式参数b(id=3)调用复制构造函数来构造局部变量a:
7: A(const A&):id=4 pre_id=3
所以a的id=4. 接着运行到return语句,需要返回一个对象,同上面介绍的类似,利用a调用复制构造函数来构造一个临时对象:
8: A(const A&):id=5 pre_id=4
此临时对象的id=5。因为已经离开函数g1,所以需要销毁局部变量a:
9: ~A():id=4 pre_id=3
然后利用临时对象id=5来构造对象b2,即:
10: A(const A&):id=6 pre_id=5
这样得到的B2的id=6.同时该临时对象任务完成,需要销毁:
11: ~A():id=5 pre_id=4
同时,之前的形式参数id=3也要销毁(这儿可以看到这个对象销毁的时间比较晚)
12: ~A():id=3 pre_id=2
这样A B2=g1(b1);这句代码就运行完毕了:B2构造完成,所有的临时变量都销毁了。
(3).对于A B3=g2(B1);
运行函数g2:
A a;
a=b;
return a;
}
g2与g1的不同之处在于g2中是进行了赋值,而不是直接调用复制构造函数来生成局部变量。
同样,因为是按值传递,所以利用B1调用复制构造函数来初始化形式参数b,
13: A(const A&):id=7 pre_id=2
形式参数b生成的局部变量的id=7。
运行A a;时调用默认构造函数来生成局部变量a
14: A():id=8 pre_id=-1
局部变量a的id=8。然后运行a=b;这儿需要调用赋值操作符:
15: =(const A&):id=8 pre_id=7
将b(id=7)赋给a。然后运行return语句,利用a来调用复制构造函数来构造一个临时对象
16: A(const A&):id=9 pre_id=8
该临时对象的id=9 。由于要离开函数,所以临时对象a(id=8)需要销毁:
17: ~A():id=8 pre_id=7
销毁完临时对象,利用临时变量id=9来调用复制构造函数来构造B3:
18: A(const A&):id=10 pre_id=9
所以得到B3(id=10)。将在调用g2中的临时变量进行销毁,先销毁临时变量id=9:
19: ~A():id=9 pre_id=8
再销毁形式参数b生成的临时变量id=7(又是最晚销毁形式参数)
20: ~A():id=7 pre_id=2
这样的话A B3=g2(b1); 这一句就运行完毕,得到了对象B3(id=10)
(4)下面几句运行的方式和前3句类似,只是:先调用默认构造函数生成对象,然后在调用赋值操作符进行赋值。
c1=f();
c2=g1(c1);
c3=g2(c1);
下面三句就先调用默认构造函数生成c1,c2,c3:
21: A():id=11 pre_id=-1
22: A():id=12 pre_id=-1
23: A():id=13 pre_id=-1
这样生成的c1(id=11),c2(id=12),c3(id=13)。
下面五句是c1=f();对应的运行结果:
24: A():id=14 pre_id=-1
25: A(const A&):id=15 pre_id=14
26: ~A():id=14 pre_id=-1
27: =(const A&):id=11 pre_id=15
28: ~A():id=15 pre_id=14
下面七句是运行c2=g1(c1);得到的结果:
29: A(const A&):id=16 pre_id=11
30: A(const A&):id=17 pre_id=16
31: A(const A&):id=18 pre_id=17
32: ~A():id=17 pre_id=16
33: =(const A&):id=12 pre_id=18
34: ~A():id=18 pre_id=17
35: ~A():id=16 pre_id=11
36: A(const A&):id=19 pre_id=11
37: A():id=20 pre_id=-1
38: =(const A&):id=20 pre_id=19
39: A(const A&):id=21 pre_id=20
40: ~A():id=20 pre_id=19
41: =(const A&):id=13 pre_id=21
42: ~A():id=21 pre_id=20
43: ~A():id=19 pre_id=11
到此的话,所有的正常的语句都运行完毕。
下面是因为main函数要返回,所以一些变量要进行销毁:
44: ~A():id=13 pre_id=21//销毁对象c3(id=13)
45: ~A():id=12 pre_id=18//销毁对象c2(id=12)
46: ~A():id=11 pre_id=15//销毁对象c2(id=11)
47: ~A():id=10 pre_id=9//销毁对象B3(id=10)
48: ~A():id=6 pre_id=5//销毁对象B2(id=6)
49: ~A():id=2 pre_id=1////销毁对象B3(id=2)
至此,程序正常结束。
对左面的运行结果,即采用RVO 的情况进行简要分析:
从运行结果看,相比不采用RVO情况,使用RVO可以优化掉很多步骤:
(1)对于A B1=f();
A a;
return a;
}
下面一句是对应的运行结果:
正常情况下应该是:在函数f中利用默认构造函数构造局部对象a,然后直接利用a调用复制构造函数来初始化B1。这样就省去了生成临时变量的情形。
而这儿跟进一步进行了优化,发现f中只是返回一个对象,所以就直接相当于用默认构造函数来初始化对象B1,这样得到B1(id=0)。连临时对象a的生成都
(2)对于A B2=g1(B1);
A a=b;
return a;
}
下面三句是运行结果:
3: A(const A&):id=2 pre_id=1
4: ~A():id=1 pre_id=0
先用B1(id=0)来初始化形参b(id=1),然后因为发现g1是直接返回局部变量a,所以省去a的生成,直接使用b(id=1)来初始化B2,得到B2(id=2)。
(3)对于A B3=g2(B1);
A a;
a=b;
return a;
}
5: A(const A&):id=3 pre_id=0
6: A():id=4 pre_id=-1
7: =(const A&):id=4 pre_id=3
8: ~A():id=3 pre_id=0
这儿是先利用B1(id=0)复制构造形式参数b(id=3),然后是直接默认构造得到B3(id=4)(相当于直接利用了a的生成),然后在调用赋值操作符,从b(id=3)得到值。最后析构了b(id=3)。
(4)对于A c1,c2,c3;
对应下面的三句:
9: A():id=5 pre_id=-1
10: A():id=6 pre_id=-1
11: A():id=7 pre_id=-1
这儿没有什么优化,直接c1(id=5),c2(id=6),c3(id=7)
(5)对于下面三句:
c2=g1(c1);
c3=g2(c1);
下面句是对应的结果:
12: A():id=8 pre_id=-1//由于需要利用f的返回值进行赋值操作,所以在f中调用默认构造函数直接生成一个临时变量(相当于省略了局部变量a)id=8
13: =(const A&):id=5 pre_id=8//利用临时变量id=8对c1(id=5)进行赋值操作。
14: ~A():id=8 pre_id=-1//临时变量id=8完成任务,销毁
15: A(const A&):id=9 pre_id=5//利用c1(id=5)复制构造形式参数b(id=9)
16: A(const A&):id=10 pre_id=9//利用形式参数b(id=9)复制构造一个临时变量id=10(省略局部变量a)
17: =(const A&):id=6 pre_id=10//利用临时变量id=10对c2(id=6)进行赋值操作
18: ~A():id=10 pre_id=9//销毁临时变量id=10
19: ~A():id=9 pre_id=5//销毁形式参数b对应临时变量id=9
20: A(const A&):id=11 pre_id=5//利用c1(id=5)复制构造形式参数b(id=11)
21: A():id=12 pre_id=-1//因为在g2函数中进行了赋值操作,并且需要返回局部变量a,这儿是省略了构造局部变量a,直接构造一个临时变量id=12
22: =(const A&):id=12 pre_id=11//利用形式参数b(id=11)对临时变量id=12进行赋值。
23: =(const A&):id=7 pre_id=12//利用临时变量id=12对c3(id=7)进行赋值。
24: ~A():id=12 pre_id=11//临时变量id=12完成任务,销毁
25: ~A():id=11 pre_id=5//销毁形式参数b对应临时变量id=11
下面就是程序即将运行完毕,销毁main函数中的所有局部变量:
26: ~A():id=7 pre_id=12//销毁对象c3(id=7)
27: ~A():id=6 pre_id=10//销毁对象c2(id=6)
28: ~A():id=5 pre_id=8//销毁对象c1(id=5)
29: ~A():id=4 pre_id=3//销毁对象B3(id=4)
30: ~A():id=2 pre_id=1//销毁对象B2(id=2)
31: ~A():id=0 pre_id=-1//销毁对象B1(id=0)
从上面的分析可以看出,进行RVO是G++编译器进行了相当多的优化 。
为了便于比较两种情况下的输出及源代码的对应关系,下图用相同颜色标示出了对应的语句: