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++編譯器進行了相當多的優化 。

爲了便於比較兩種情況下的輸出及源代碼的對應關係,下圖用相同顏色標示出了對應的語句:



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章