C++中返回对象的情形及RVO

之前有文章介绍过临时对象和返回值优化RVO方面的问题。见此处

在C++中,返回对象这一点经常被诟病,因为这个地方的效率比较低,需要进行很多的操作,生成一些临时对象,如果对象比较大的会就会比较耗时。但是在编译器实现的时候,经常是对返回对象的情况进行优化,也就是进行返回值优化

在g++中,这个是默认已经进行了优化。以前我希望看看到底C++怎么操作的,但是无法看到,就是因为G++进行了默认的返回值优化RVO。今天在晚上发现可以有一中方法来禁止这个RVO,可以参考这儿

具体来说就是在编译的时候,加上-fno-elide-constructors这个选项,即:

 

g++ -o rvo_test rvo_test.cc -fno-elide-constructors

下面是一个示例,来演示C++在返回对象的时候所做的优化。

代码如下:

#include <iostream>
#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的情形,所以右边是我们需要看的,需要分析的类的真正的执行过程。同时为了便于标示比较,对运行的每一行进行了标号:

  对右面的运行结果进行解析如下:

(1). 对于 A B1=f();

先调用函数f(),在函数f中:

 A f(){
     A a;
     return a;
 }  

先调用A的默认构造函数A()生成局部对象a:

 1: A():id=0 pre_id=-1

此时a的id=0,然后因为f的返回值是一个A的对象,此时C++会利用a来调用复制构造函数来生成一个临时对象:

 2: A(const A&):id=1 pre_id=0

此临时对象的id=1, 同时因为要离开函数f所以需要析构局部对象a:

 3: ~A():id=0 pre_id=-1

在主函数main中,对象B1是利用函数f的返回值来进行初始化的:

A B1=f();

所以调用复制构造函数来对B1进行初始化:  

 4: A(const A&):id=2 pre_id=1

这样的话B1的id=2,而之前的id=1临时对象因为已经完成了任务,所以C++对其进行了析构:

 5: ~A():id=1 pre_id=0

这样B1对象就构造完毕了。 在这个过程中,对于返回值为对象(非引用或指针的情形)C++所采取的最原始的办法就是先构造一个临时对象来保存返回值,然后再利用这个临时对象来进行操作。当这个临时对象的任务完成之后就将其销毁了。

(2).对于A B2=g1(B1);

先调用函数g1

 A g1(A b){
     A a=b;
     return a;
 }

可以看到g1函数是带参数的,这其中也牵扯到传值参数的问题。由于是按值传递,所以需要进行复制,调用复制构造函数。

所以,首先调用复制构造函数,利用B1来构造g1函数中的形式参数b:  

 6: A(const A&):id=3 pre_id=2

此时形式参数作为一个局部变量,其id=3。接着运行 :

A a=b;

这一句是利用形式参数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 g2(A b){
    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句类似,只是:先调用默认构造函数生成对象,然后在调用赋值操作符进行赋值。

A c1,c2,c3;
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

下面八句是运行c3=g2(c1);得到的结果:

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 f(){
    A a;
    return a;
}

下面一句是对应的运行结果:

 1: A():id=0 pre_id=-1

正常情况下应该是:在函数f中利用默认构造函数构造局部对象a,然后直接利用a调用复制构造函数来初始化B1。这样就省去了生成临时变量的情形。

而这儿跟进一步进行了优化,发现f中只是返回一个对象,所以就直接相当于用默认构造函数来初始化对象B1,这样得到B1(id=0)。连临时对象a的生成都

(2)对于A B2=g1(B1); 

A g1(A b){
    A a=b;
    return a;

下面三句是运行结果:

 2: A(const A&):id=1 pre_id=0

 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 g2(A b){
    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)对于下面三句:

 c1=f();
 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++编译器进行了相当多的优化

为了便于比较两种情况下的输出及源代码的对应关系,下图用相同颜色标示出了对应的语句:


 

posted @ 2012-07-21 11:23  Mr.Rico  阅读(2330)  评论(0编辑  收藏  举报