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

转载至:http://www.cnblogs.com/xkfz007/archive/2012/07/21/2602110.html                                           http://www.cnblogs.com/xkfz007/articles/2506022.html

  

什么是临时对象?

        C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象.

通常出现在以下两种情况:

(1)为了使函数调用成功而进行隐式类型转换的时候

        传递某对象给一个函数,而其类型与函数的形参类型不同时,如果可以通过隐式转化的话可以使函数调用成功,那么此时会通过构造函数生成一个临时对象,当函数返回时临时对象即自动销毁。如下例:

//计算字符ch在字符串str中出现的次数 
int countChar (const string& str, char ch); 
char buffer[]; 
char c; 
//调用上面的函数
 countChar (buffer, c);

      我们看的第一个参数为char[],而函数的参数类型为const string&,参数不一致,看看能否进行隐式转化,string类有个构造函数是可以作为隐式转化函数(参见5)的。那么编译器会产生一个 string的临时变量,以buffer为参数进行构造,那么countChar中的str参数会绑定到此临时变量上,直到函数返回时销毁。

      注意这样的转化只会出现在两种情况下:函数参数以传值(by value)的方式传递 或者 对象被传递到一个 reference-to-const 参数上。

传值方式:

int countChar (string str, char ch); 
string buffer;
 char c;
 //参数通过传值方式传递 
countChar (buffer, c);

       这种方法会调用string的拷贝构造函数生成一个临时变量,再将这个临时变量绑定到str上,函数返回时进行销毁。

传常量引用:

       开始的实例即时属于这种情况,但一定强调的是传递的是const型引用,如将开始函数的原型改为

int countChar (string& str, char ch);

       下面调用相同,编译器会报错!为什么C++设计时要求当对象传递给一个reference-to-non-const 参数不会发生隐式类型转化呢?

       下面的实例可能向你说明这样设计的目的:

//声明一个将str中字符全部转化为大写 
void toUpper (string& str); 
char buffer[] = "hazirguo"; 
toUpper(buffer);                 //error!!非const引用传递参数不能完成隐式转化

        如果编译器允许上面的传递完成,那么,会生成一个临时对象,toUpper函数将临时变量的字符转化为大写,返回是销毁对象,但是对buffer内容毫无影响!程序设计的目地是期望对“非临时对象”进行修改,而如果对reference-to-non-cosnt对象进行转化,函数只会对临时变量进行修 改。这就是为什么C++中要禁止non-const-reference参数产生临时变量的原因了。

(2)当函数返回对象的时候

        当函数返回一个对象时,编译器会生成一个临时对象返回,如声明一个函数用来合并两个字符串:

const string strMerge (const string s1, const string s2);
大多时候是无法避免这样的临时变量产生的,但是现代编译器可以将这样的临时变量进行优化掉,这样的优化策略中,有个所谓的“返回值优化”,下一篇具体讲解。
 总结:
临时对象有构造和析构的成本,影响程序的效率,因此尽可能地消除它们。而更为重要的是很快地发现什么地方会生成临时对象:
  • 当我们看到一个reference-to-const参数时,极可能一个临时对象绑定到该参数上;
  • 当我们看到函数返回一个对象时,就会产生临时对象。
参考: http://www.cnblogs.com/hazir/archive/2012/04/18/2456144.html  

C++中的返回值优化(return value optimization)

返回值优化(Return Value Optimization,简称RVO),是这么一种优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用户返回,那么这个临时对象会消 耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价,下面是 在Visual Studio 2008的Debug模式下做的一个测试:(在GCC下测试的时候可能编译器自己进行了RVO优化,看不到两种代码的区别) 
复制代码
//  C++ Return Value Optimization
//  作者:代码疯子
//  博客: http://www.programlife.net/
#include <iostream>
using  namespace std;
class Rational
{
public:
    Rational( int numerator =  0int denominator =  1) : n(numerator), d(denominator) {
          cout <<  " Constructor Called... " << endl;
      }
      ~Rational() {
          cout <<  " Destructor Called... " << endl;
      }
      Rational( const Rational& rhs) {
           this->d = rhs.d;
           this->n = rhs.n;
          cout <<  " Copy Constructor Called... " << endl;
      }
       int numerator()  const {  return n; }
       int denominator()  const {  return d; }
private:
     int n, d;
}; 
const Rational  operator*( const Rational& lhs,  const Rational& rhs) {
    cout <<  " ----------- Enter operator* ----------- " << endl;
    Rational tmp(lhs.numerator() * rhs.numerator(),
        lhs.denominator() * rhs.denominator());
    cout <<  " ----------- Leave operator* ----------- " << endl;
     return tmp;
}
int main( int argc,  char **argv) {
    Rational x( 15), y( 29);
    Rational z = x * y;
    cout <<  " calc result:  " << z.numerator() 
        <<  " / " << z.denominator() << endl;
 
     return  0;
}
复制代码

函数输出截图如下:
Return Value Optimization
可以看到消耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。

而如果把operator*换成另一种形式:

const Rational  operator*( const Rational& lhs, const Rational& rhs)
{
     return Rational(lhs.numerator() * rhs.numerator(),
                lhs.denominator() * rhs.denominator());
}

就只会消耗一个构造函数的成本了:
返回值优化

参考: http://www.programlife.net/cpp-return-value-optimization.html

返回值优化(RVO)与具命返回值优化(NRVO)

这是一项编译器做的优化,已经是一种很常见的优化手段了,搜一下可以找到很多的资料,在MSDN 里也有相关的说明。

返回值优化,顾名思义,就是与返回值有关的优化,是当函数是按值返回(而不是引用、指针)时,为了避免产生不必要的临时对象以及值拷贝而进行的优化。

先看看下面的代码:

复制代码
typedef unsigned  int UINT32;
class MyCla
{
public:
    MyCla(UINT32 a_size =  10):size(a_size) {
        p =  new UINT32[size];        
    }
    MyCla(MyCla  const & a_right):size(a_right.size) {
        p =  new UINT32[size];
        memcpy(p, a_right.p, size* sizeof(UINT32));
    }
    MyCla  constoperator = (MyCla  const & a_right) {
        size = a_right.size;
        p =  new UINT32[size];
        memcpy(p, a_right.p, size* sizeof(UINT32));
         return * this;
    }
    ~MyCla() {
        delete [] p;
    }
private:
    UINT32 *p;
    UINT32 size;
};
MyCla TestFun() {
     return MyCla();
}
int _tmain( int argc, _TCHAR* argv[])
{
    MyCla a = TestFun();   
     return  0;
}
复制代码
TestFun() 函数返回了一个 MyCla 对象,而且是按值传递的。

在没有任何“优化”之前,这段代码的行为也许是这样的:return MyCla() 这行代码中,构造了一个 MyCla 类的临时的无名对象(姑且叫它t1),接着把 t1 拷贝到另一块临时对象 t2(不在栈上),然后函数保存好 t2 的地址(放在 eax 寄存器中)后返回,TestFun 的栈区间被“撤消”(这时 t1 也就“没有”了,t1 的生存域在 TestFun 中,所以被析构了),在 MyCla a = TestFun(); 这一句中,a 利用 t2 的地址,可以找到 t2 进行,接着进行构造。这样 a 的构造过程就完成了。然后再把 t2 也“干掉”。

可以看到, 在这个过程中,t1 和 t2 这两个临时的对象的存在实在是很浪费的,占用空间不说,关键是他们都只是为a的构造而存在,a构造完了之后生命也就终结了。既然这两个临时的对象对于程序 员来说根本就“看不到、摸不着”(匿名对象),于是编译器干脆在里面做点手脚,不生成它们!怎么做呢?很简单,编译器“偷偷地”在我们写的TestFun 函数中增加一个参数 MyCla&,然后把 a 的地址传进去(注意,这个时候 a 的内存空间已经存在了,但对象还没有被“构造”,也就是构造函数还没有被调用),然后在函数体内部,直接用a来代替原来的“匿名对象”,在函数体内部就完 成a的构造。这样,就省下了两个临时变量的开销。这就是所谓的“返回值优化”!在 VC7 里,按值返回匿名对象时,默认都是这么做。

上 面说的是“返回值优化(RVO)”,还有一种“具名返回值优化(NRVO)”,是对于按值返回“具名对象”(就是有名字的变量!)时的优化手段,其实道理 是一样的,但由于返回的值是具名变量,情况会复杂很多,所以,能执行优化的条件更苛刻,在下面三种情况下(来自 MSDN),NRVO 将一定不起作用:

  1. 不同的返回路径上返回不同名的对象(比如if XXX 的时候返回x,else的时候返回y)
  2. 引入 EH 状态的多个返回路径(就算所有的路径上返回的都是同一个具名对象)
  3. 在内联asm语句中引用了返回的对象名。

不过就算 NRVO 不能进行,在上面的描述中的 t2 这个临时变量也不会产生,对于 VC 的 C++ 编译器来说,只要你写的程序是把对象按值返回的,它会有两种做法,来避免 t2 的产生。拿下面这个程序来说明:

MyCla TestFun2() {
    MyCla x( 3);
     return x;
}
一种做法是像 RVO一样,把作为表达式中获取返回值来进行构造的变量 a 当成一个引用参数传入函数中,然后在返回语句之前,用要返回的那个变量来拷贝构造 a,然后再把这个变量析构,函数返回原调用点,a 就构造好了。

还有一种方式, 是在函数返回的时候,不析构x,而直接把x的地址放到 exa 寄存器中,返回调到 TestFun2 的调用点上,这时,a 可以用 exa 中存着的地址来进行构造,a 构造完成之后,再析构原来的变量 x !是的,注意到其实这时,x 的生存域已经超出了TestFun2,但由于这里x所在TestFun2的栈虽然已经无效,但是并没有谁去擦写这块存,所以x其实还是有效的,当然,一切 都在汇编的层面,对于C++语言层面来讲是透明的。

在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++编译器进行了相当多的优化 。

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



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值