2011-01-26 36 views
10

想象一下這樣的情況,我有這樣的功能:創建和從函數返回一個大對象

Object f() 
{ 
    Object obj; 
    return obj; 
} 

sizeof(Object)是一個很大的值。

然後,我讓這個函數的調用:

Object object = f(); 

我是否正確理解,第一個目的堆棧(在功能)上創建,然後將被複制到對象變量?

如果是這樣,是否合理地在堆上的函數中創建一個對象並返回一個指向它的指針而不是副本?

但我的意思是該對象必須在f()函數中創建 - 沒有通過指針或引用此函數並初始化。

編輯

我的意思不是說f是一個很簡單的功能。根據某些上下文,它可以有一個非常複雜的對象初始化例程。編譯器是否仍然會優化它?

+0

技術上是的。但是對於這樣一個簡單的函數,編譯器會優化拷貝。構建代碼在構造函數/複製構造函數和析構函數中放置打印語句,然後在不進行優化並完全優化的情況下構建代碼,並查看執行了多少個打印語句。 – 2011-01-26 19:38:37

+0

那麼,如果這是C,那麼這只是一個對malloc的調用。如果它是C++,那麼你會新的對象。大概你的函數在返回之前對對象做了一些事情,否則它就沒有任何用處。你爲什麼把它標記爲C和C++?他們是不同的。 – 2011-01-26 19:39:13

+0

c和C++因爲它可能是一個結構體或一個類。所以這種情況可能會出現在兩種語言中 – Andrew 2011-01-26 19:41:51

回答

18

對於這種特殊情況,您可以充分利用現今編譯器足夠聰明的優勢。該優化被稱爲,名爲return value optimization(NRVO),因此可以返回這樣的「大」對象。編譯器可以看到這樣的機會(特別是在代碼片段那樣簡單的情況下)並生成二進制文件,以便不會創建副本。

您還可以返回無名臨時對象:

Object f() 
{ 
    return Object(); 
} 

這將調用(未命名)返回值優化(RVO)在幾乎所有的現代C++編譯器。事實上,即使所有的優化都被關閉,Visual C++也實現了這種特殊的優化。

這些種類的優化特別通過C++標準允許的:

ISO 14882:2003 C++標準,§12.8段。 15: 複製類對象

當滿足特定條件時,一個 實現允許省略類對象的 拷貝構造, 即使複製構造和/或 析構函數爲對象具有側 效果。在這種情況下, 執行將源和省略複製操作的 目標 作爲簡單的 兩種不同的方式指代相同的對象,並且 破壞該對象的發生 當兩個 對象時代的後面在沒有優化的情況下會被銷燬 。拷貝操作的這個elison 在 下列情況下才允許(其可以是 組合以消除多個 份):在與類terturn類型的功能的return聲明

  • , 當表達式是與 相同CV-不合格類型作爲 函數返回類型的 非易失性自動對象的名稱,拷貝 操作可以通過 構造自動對象 直接進入函數的返回被省略值
  • 當尚未結合至參考 臨時類對象將被複制到一個類對象與 相同CV-unqualitied型,複製 操作可以通過 直接構建臨時對象 可以省略進入 省略副本的目標。

一般來說,編譯器會一直嘗試實施NRVO和/或視網膜靜脈阻塞,但它可能無法在某些情況下這樣做,就像多個返回路徑。不過,這是一個非常有用的優化,你不應該害怕使用它。

如果有疑問,可以隨時通過插入「調試語句」測試你的編譯器,並看到自己:

class Foo 
{ 
public: 
    Foo()      { ::printf("default constructor\n"); } 
    // "Rule of 3" for copyable objects 
    ~Foo()      { ::printf("destructor\n");   } 
    Foo(const Foo&)   { ::printf("copy constructor\n"); } 
    Foo& operator=(const Foo&) { ::printf("copy assignment\n");  } 
}; 

Foo getFoo() 
{ 
    return Foo(); 
} 

int main() 
{ 
    Foo f = getFoo(); 
} 

如果返回的對象並不意味着可拷貝,或者(N)RVO失敗(這可能是不太可能發生),那麼你可以嘗試返回一個代理對象:

struct ObjectProxy 
{ 
private: 
    ObjectProxy() {} 
    friend class Object; // Allow Object class to grab the resource. 
    friend ObjectProxy f(); // Only f() can create instances of this class. 
}; 

class Object 
{ 
public: 
    Object() { ::printf("default constructor\n"); } 
    ~Object() { ::printf("destructor\n"); } 
    // copy functions undefined to prevent copies 
    Object(const Object&); 
    Object& operator=(const Object&); 
    // but we can accept a proxy 
    Object(const ObjectProxy&) 
    { 
     ::printf("proxy constructor\n"); 
     // Grab resource from the ObjectProxy. 
    } 
}; 

ObjectProxy f() 
{ 
    // Acquire large/complex resource like files 
    // and store a reference to it in ObjectProxy. 
    return ObjectProxy(); 
} 

int main() 
{ 
    Object o = f(); 
} 

當然,這是不完全明顯,將需要使適當的文件(至少一個關於它的評論)。

您還可以將某種智能指針(如std::auto_ptrboost::shared_ptr或類似的東西)返回給免費商店中分配的對象。如果您需要返回派生類型的實例,則需要此操作:

class Base {}; 
class Derived : public Base {}; 

// or boost::shared_ptr or any other smart pointer 
std::auto_ptr<Base> f() 
{ 
    return std::auto_ptr<Base>(new Derived); 
} 
2

從理論上講,您所描述的是應該發生的事情。無論如何,編譯器通常能夠以某種方式優化它,即使用調用者的Objectf將直接寫入調用者的對象並返回null。

這被稱爲Return Value Optimization(或RVO)

2

我是否正確地理解,第一 對象將在堆棧​​上被創建(在 函數),然後將被複制到 對象變量?

是在堆棧上創建了obj,但是當您返回稱爲返回值優化或RVO的進程時,可以防止不必要的副本。

如果是這樣,是不是合理對堆創建功能的 對象和 到返回一個指向它,而不是 副本?

是的,只要清楚地記錄客戶端負責清理內存,就可以在堆上創建一個對象並返回一個指針。

但是,返回一個智能指針(如shared_ptr<Object>)會比合理更好,這會減少客戶端必須記住明確釋放內存。

2

編譯器會優化它。

除了在某些情況下,such as

std::string f(bool cond = false) 
{ 
    std::string first("first"); 
    std::string second("second"); 
    // the function may return one of two named objects 
    // depending on its argument. RVO might not be applied 
    if(cond) 
    return first; 
    else 
    return second; 
} 

當然可以有一些舊的編譯器,它可以調用拷貝構造函數。但你不應該用現代編譯器來擔心它。

1

如果你的函數f是一個工廠方法,最好返回一個指針或初始化的智能指針對象,比如auto_ptr。

auto_ptr<Object> f() 
{ 
    return auto_ptr<Object>(new Object); 
} 

使用方法:編譯

{  
    auto_ptr<Object> myObjPtr = f(); 
    //use myObjPtr . . . 
} // the new Object is deleted when myObjPtr goes out of scope 
2

是否可以申請RVO取決於所涉及的實際代碼。一般指導原則是儘可能晚地創建返回值。例如:

std::string no_rvo(bool b) { 
    std::string t = "true", f = "fals"; 

    f += t[3]; // Imagine a "sufficiently smart compiler" couldn't delay initialization 
    // for some reason, such not noticing only one object is required depending on some 
    // condition. 

    //return (b ? t : f); // or more verbosely: 
    if (b) { 
    return t; 
    } 
    return f; 
} 

std::string probably_rvo(bool b) { 
    // Delay creation until the last possible moment; RVO still applies even though 
    // this is superficially similar to no_rvo. 
    if (b) { 
    return "true"; 
    } 
    return "false"; 
} 

用C++ 0x中,編譯器可以自由地做出更加的假設,主要由能夠使用移動語義。這些工作如何成爲蠕蟲的一員,但移動語義正在設計中,以便它們可以應用於上面的確切代碼。這在no_rvo情況下最顯着,但它在兩種情況下都提供了有保證的語義,因爲移動操作(如果可能)優於複製操作,而RVO完全是可選的並且不容易檢查。

1

我不知道爲什麼沒有人指出明顯的解決方案。只需按引用傳遞的輸出對象:

void f(Object& result) { 
    result.do_something(); 
    result.fill_with_values(/* */); 
}; 

這樣:

  • 你避免副本是肯定的。

  • 你避免使用堆。

  • 您避免讓調用代碼負責釋放動態分配的對象(儘管shared_ptr或unique_ptr也會這樣做)。

另一種方法是使函數的Object一員,但可能並不合適,這取決於f()的合同是什麼。