在C ++計算機編程中,複製省略指的是一種編譯器最佳化技術,可以消除不必要的對象複製。 C ++語言標準通常允許實現執行任何最佳化,前提是結果程式的可觀察行為與假定完全按照標準強制執行的程式相同,即假裝。
該標準還描述了一些情況,即即使這會改變程式的行為也可以消除複製,最常見的是返回值最佳化。在C ++標準中描述的另一種廣泛實施的最佳化是,類類型的臨時對象被複製到相同類型的對象上。因此,複製初始化通常等同於性能方面的直接初始化,而不是語義;複製初始化仍然需要一個可訪問的複製構造函式。最佳化不能套用於已綁定到引用的臨時對象。
基本介紹
- 中文名:複製省略
- 外文名:Copy elision
例子,返回值最佳化,概要,背景,編譯器支持,
例子
#include <iostream>int n = 0;struct C {explicit C(int) {}C(const C&) { ++n; } // the copy constructor has a visible side effect}; // it modifies an object with static storage durationint main() {C c1(42); // direct-initialization, calls C::C(42)C c2 = C(42); // copy-initialization, calls C::C( C(42) )std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwisereturn 0;}
根據標準,類似的最佳化可以套用於被拋出和被捕獲的對象,但是最佳化是否適用於從被拋出對象到異常對象的複製以及從異常複製 對象是catch子句的exception-declaration中聲明的對象。 還不清楚這種最佳化是否僅適用於臨時對象或命名對象。鑒於以下原始碼:
#include <iostream>struct C {C() {}C(const C&) { std::cout << "Hello World!\n"; }};void f() {C c;throw c; // copying the named object c into the exception object.} // It is unclear whether this copy may be elided (omitted).int main() {try {f();} catch (C c) { // copying the exception object into the temporary in the// exception declaration.} // It is also unclear whether this copy may be elided (omitted).}
因此,符合標準的編譯器應該生成一個列印“Hello World!”的程式。 兩次。 在當前版本的C ++標準(C ++ 11)中,已經解決了這些問題,基本上允許從命名對象到異常對象的複製,以及在異常處理程式中聲明的對象的複製被省略。
GCC提供了-fno-elide-constructors選項來禁用copy-elision。 此選項可用於觀察(或不觀察)返回值最佳化或其他最佳化複製的效果。 通常不建議禁用這個重要的最佳化。
返回值最佳化
在C ++程式語言的上下文中,返回值最佳化(RVO)是一種編譯器最佳化,它涉及消除為保存函式返回值而創建的臨時對象。 RVO特別值得注意的是允許通過C ++標準改變結果程式的可觀察行為。
概要
通常,C ++標準允許編譯器執行任何最佳化,只要生成的執行檔表現出相同的可觀察行為,就好像(即假裝)已滿足標準的所有要求一樣。這通常被稱為“as-if規則”。[8]術語返回值最佳化是指C ++標準中的一個特殊子句,它比“as-if”規則更進一步:實現可以省略由return語句產生的複製操作,即使複製構造函式有副作用。
下面的例子演示了一個場景,其中實現可以消除一個或兩個副本,即使副本構造函式有可見的副作用(列印文本)。可以消除的第一個副本是可以將無名臨時C複製到函式f的返回值中的副本。可以消除的第二個副本是由f返回給obj的臨時對象的副本。
#include <iostream>struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; }};C f() { return C();}int main() { std::cout << "Hello World!\n"; C obj = f(); return 0;}
根據編譯器和編譯器的設定,生成的程式可能會顯示以下任何輸出:
Hello World!A copy was made.A copy was made.
Hello World!A copy was made.
Hello World!
背景
從函式返回一個內置類型的對象通常幾乎沒有開銷,因為對象通常適合CPU暫存器。 返回一個更大類的對象可能需要從一個記憶體位置到另一個記憶體位置的更昂貴的複製。 為了避免這種情況,一個實現可能會在調用者的堆疊框架中創建一個隱藏對象,並將該對象的地址傳遞給該函式。 函式的返回值然後被複製到隱藏的對象中。[9] 因此,這樣的代碼:
struct Data { char bytes[16]; };Data f() { Data result = {}; // generate result return result;}int main() { Data d = f();}
可以生成與此相當的代碼:
struct Data { char bytes[16]; };Data * f(Data * _hiddenAddress) { Data result = {}; // copy result into hidden object *_hiddenAddress = result; return _hiddenAddress;}int main() { Data _hidden; // create hidden object Data d = *f(&_hidden); // copy the result into d}
這會導致Data對象被複製兩次。
在C ++發展的早期階段,語言無法有效地從函式中返回類類型的對象被認為是一個弱點。1991年前後,Walter Bright實施了一種技術來最大限度地減少複製,有效地用隱藏對象和函式內的命名對象替換用於保存結果的對象:
struct Data { char bytes[16]; };void f(Data *p) { // generate result directly in *p}int main() { Data d; f(&d);}
Bright在他的Zortech C ++編譯器中實現了這種最佳化。這種特殊技術後來被稱為“命名返回值最佳化”,指的是省略了對命名對象的複製這一事實。
編譯器支持
大多數編譯器都支持返回值最佳化。但是,編譯器可能無法執行最佳化。 一種常見的情況是函式可能會根據執行路徑返回不同的命名對象:
#include <string>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 return cond ? first : second;}int main() { std::string result = f();}