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

http://www.cnblogs.com/xkfz007/archive/2012/07/21/2602110.html

之前有文章介绍过临时对象和返回值优化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++编译器进行了相当多的优化 。

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

时间: 2024-10-05 23:54:31

(转)C++中返回对象的情形及RVO的相关文章

转:C++中临时对象及返回值优化

http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象. 通常出现在以下两种情况: (1)为了使函数调用成功而进行隐式类型转换的时候. 传递某对象给一个函数,而其类型与函数的形参类型不同时,如果可以通过隐式转化的话可以使函数调用成功,那么此时会通过构造函数生成一个临时对象,当函数返回时临时对象即自动销毁.如下例: //计算字符ch

SpringMVC中通过@ResponseBody返回对象,Js中调用@ResponseBody返回值,统计剩余评论字数的js,@RequestParam默认值,@PathVariable的用法

1.SpringMVC中通过@ResponseBody返回对象,作为JQuery中的ajax返回值 package com.kuman.cartoon.controller; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.apache.commons.io.filefilter.FalseFileFilter; import org.slf4j.Logger; import org.s

[转] C++中临时对象及返回值优化

http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象. 通常出现在以下两种情况: (1)为了使函数调用成功而进行隐式类型转换的时候. 传递某对象给一个函数,而其类型与函数的形参类型不同时,如果可以通过隐式转化的话可以使函数调用成功,那么此时会通过构造函数生成一个临时对象,当函数返回时临时对象即自动销毁.如下例: //计算字符ch

对象处理方法, 首先内存中要要有对象原形 才可以返回对象

struct op( name = "ccc") coc = #( cc = op name: 20 , op name: "name" ) --- 保存数组配置 o = gt_max_namespace.gt_cl_hp_fileiooutin(); o. setfile "c://t.txt" coc ccc= o.getfile "c://t.txt" --#("(op name:20)", &quo

使用C语言为python编写动态模块(2)--解析python中的对象如何在C语言中传递并返回

楔子 编写扩展模块,需要有python源码层面的知识,我们之前介绍了python中的对象.但是对于编写扩展模块来讲还远远不够,因为里面还需要有python中模块的知识,比如:如何创建一个模块.如何初始化python环境等等.因此我们还需要了解一些前奏的知识,如果你的python基础比较好的话,那么我相信你一定能看懂,当然我们一开始只是介绍一个大概,至于细节方面我们会在真正编写扩展模块的时候会说. 关于使用C为python编写扩展模块,我前面还有一篇博客,强烈建议先去看那篇博客,对你了解Pytho

EBS OAF开发中实体对象和视图对象的属性设置器

(版权声明,本人原创或者翻译的文章如需转载,如转载用于个人学习,请注明出处:否则请与本人联系,违者必究) 源文: Home > Oracle Application Framework Documentation Set, Release 12.2 > Oracle Application Framework Developer's Guide > Chapter 5: Implementing Server-Side Features > Entity Object and Vi

Serializable在C#中的作用.net中的对象序列化 (转)

序列化是指将对象实例的状态存储到存储媒体的过程,在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转 换为字节流,然后再把字节流写入数据流,在随后对对象进行反序列化时,将创建出与原对象完全相同的副本. 在面向对象的环境中实现序列化机制时,必须在易用性和灵活性之间进行一些权衡.只要您对此过程有足够的控制能力,就可以使该过程在很大程度上自动进行.例如,简单的二进制序列化不能满足需要,或者,由于特定原因需要确定类中那些字段需要序列化.以下各部分将探讨 .NET 框架提供的可靠的

js中推断对象详细类型

大家可能知道js中推断对象类型能够用typeof来推断. 看以下的情况 <script> alert(typeof 1);//number alert(typeof "2");//string alert(typeof [1,2,3]);//object alert(typeof {"name":"zhuhui"})//object </script> 从上面中我们能够看出数组和普通对象用typeof推断出来都是objec

ASP.NET中Server对象

1.asp.net中常见对象: Request:服务器读取浏览器请求中的信息. Response:将服务器中的信息发送给浏览器. Server:获取请求服务器的相关信息. Application:应用程序级对象,多用户之间可以共享数据. Session:会话,用户通过网址访问服务器时会话启动. 下面的图可以简单理解为:我们发布的服务器为Server对象,在服务器上运行的web程序为Application对象,每个客户端对web程序的一个访问就是一个Session 2.Global.aspx 初始