天天看點

Google C++每周貼士 #166: 你不是真正的複制每周貼士 #166: 你不是真正的複制

(原文連結: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

繼續閱讀