(原文連結:https://abseil.io/tips/166 譯者:[email protected])
每周貼士 #166: 你不是真正的複制
- 最初釋出于:2019-08-28
- 作者:Richard Smith
- 更新于:2020-04-06
- 短連結:abseil.io/tips/166
Entia non sunt multiplicanda praeter necessitatem." (“Entities should not be multiplied without necessity”) – William of Ockham
如無必要,勿增實體——奧卡姆
If you don’t know where you’re going, you’re probably going wrong.” – Terry Pratchett
如果你不知道要去哪兒,那你可能是走錯了——特裡·普拉切特
概述
從C++17開始,對象會盡可能在“原地”被建立。
class BigExpensiveThing {
public:
static BigExpensiveThing Make() {
// ...
return BigExpensiveThing();
}
// ...
private:
BigExpensiveThing();
std::array<OtherThing, 12345> data_;
};
BigExpensiveThing MakeAThing() {
return BigExpensiveThing::Make();
}
void UseTheThing() {
BigExpensiveThing thing = MakeAThing();
// ...
}
這段代碼複制或移動了
BigExpensiveThing
多少次?
在C++17以前,答案是最多三次:每個
return
語句一次,還有一次是在初始化
thing
的時候。這有點道理:每個函數都有可能把
BigExpensiveThing
放到不同的地方,是以也許需要一次移動,來把值放到最終調用者需要的地方。然而在實踐中,對象總是“原地”構造在變量
thing
裡,沒有發生任何移動,而且C++語言允許這些移動操作被“省略”,以促成此優化。
在C++17裡,這段代碼保證零次複制或移動。實際上,就算
BigExpensiveThing
是不可移動的,上述代碼仍然是合法的。
BigExpensiveThing::Make
中的構造函數調用,直接在
UseTheThing
裡構造本地變量
thing
。
是以發生了什麼?
當編譯器看到形如
BigExpensiveThing()
的表達式的時候,它并不會馬上建立一個臨時對象。取而代之的是,它把這個表達式當做構造某個最終對象的施工圖,然後把建立(正式地說,“實作”)臨時對象的操作推遲得越長越好。
一般情況下,建立對象會被一直推遲,直到對象被取名。該命名對象(上面例子中的
thing
)是直接被運作初始化器所得到的施工圖所初始化的。如果該名字是一個引用,那麼一個臨時對象會被實作出來以承載該值。
結果就是,對象在正确的位置被直接構造,而不是在别處構造再複制過來。該行為有時被稱作“保證執行的複制省略”,但那是不準确的:從來就沒有過複制。
你需要知道的就是:對象在被首次被取名前不會被複制。以值傳回沒有額外代價。
(就算是被取名以後,函數本地變量被傳回時也不會被複制,因為有命名傳回值優化。詳情請參考Tip 11。)
繁瑣細節:匿名對象何時被複制
在兩個邊緣情況下,使用匿名對象還是會導緻複制:
- 構造基類:在基類構造函數初始化清單中,即使是以基類類型的未命名表達式來構造,還是會發生複制。這是因為類型作為基類時,可能有稍微不同的布局和表現(因為虛基類和虛函數指針值),是以直接初始化基類也許不會得到正确的表現。
class DerivedThing : public BigExpensiveThing { public: DerivedThing() : BigExpensiveThing(MakeAThing()) {} // 可能複制data_ };
- 傳遞或傳回小而簡單的對象:如果一個足夠小且可以平凡複制(trivially copyable)的對象,被傳遞進或傳回出函數,那麼它可能會被傳遞進寄存器,是以在傳遞前後可能會有不同的位址。
struct Strange { int n; int *p = &n; }; void f(Strange s) { CHECK(s.p == &s.n); // 可能炸鍋 } void g() { f(Strange{0}); }
繁瑣細節:值類别
C++中有兩種風格的表達式:
- 得到一個值,例如
,或1
——你可能會認為擁有非引用類型的表達式。MakeAThing()
- 得到一個指向存在的對象的位置,例如
或s
——你可能會認為擁有引用類型的表達式。thing.data_[5]
這個分割被稱為“值類别”;前者是 純右值(prvalues) 而後者是 泛左值(glvalues)。當我們前面探讨匿名對象的時候,我們實際上指的是純右值表達式。
所有的純右值表達式都在決定值存儲位置的上下文中被求值,而純右值表達式的執行過程則被用以在該位置初始化該值。
例如,在
中,純右值表達式
MakeAThing()
作為變量
thing
的初始化器被求值,是以
MakeAThing()
會直接初始化
thing
。構造函數把指向
thing
的指針傳遞給
MakeAThing()
,然後
MakeAThing()
中的
return
語句初始化該指針指向的玩意兒。相似地,在
中,編譯器有個指針指向待初始化的對象,并且直接調用
BigExpensiveThing
的構造函數來初始化該對象。
相關閱讀
- Tip #11: Return Policy
- Tip #24: Copies, Abbrv.
- 每周貼士 #77: 臨時變量,移動,和複制
- Tip #117: Copy Elision and Pass-by-Value