1. 程式人生 > >C++中臨時物件及返回值優化

C++中臨時物件及返回值優化

什麼是臨時物件?

        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引數時,極可能一個臨時物件繫結到該引數上;
  • 當我們看到函式返回一個物件時,就會產生臨時物件。

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 = 0, int 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(1, 5), y(2, 9);
    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());
}

就只會消耗一個建構函式的成本了:
返回值優化

返回值優化(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 const& operator = (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++語言層面來講是透明的。