跳轉到內容

更多 C++ 習語/計算建構函式

來自華夏公益教科書,開放的書籍,為開放的世界

計算建構函式

[編輯 | 編輯原始碼]
  • 最佳化按值返回
  • 允許在無法處理命名返回值最佳化 (NRVO) 的編譯器上進行返回值最佳化 (RVO)

也稱為

[編輯 | 編輯原始碼]

在 C++ 中,按值返回大型 C++ 物件代價很高。當局部建立的物件按值從函式返回時,會在堆疊上建立一個臨時物件。臨時物件通常很短命,因為它們要麼被分配給其他物件,要麼被傳遞給其他函式。臨時物件通常在建立它們的語句完全執行後超出範圍並因此被銷燬。

多年來,編譯器一直在發展,以應用一些最佳化來避免建立臨時物件,因為這通常是浪費的,並且會損害效能。返回值最佳化 (RVO) 和命名返回值最佳化 (NRVO) 是兩種流行的編譯器技術,它們試圖最佳化掉臨時物件(也稱為複製消除)。RVO 的簡要解釋如下。

返回值最佳化

以下示例演示了一種場景,即使複製建構函式具有可見副作用(列印文字),實現也可以消除其中一個或兩個副本。可以消除的第一個副本是將 Data(c) 複製到函式 func 的返回值中。可以消除的第二個副本是將 func 返回的臨時物件複製到 d1 中。有關 RVO 的更多資訊,請訪問 維基百科

struct Data {
  Data(char c = 0) 
  { 
    std::fill(bytes, bytes + 16, c); 
  }
  Data(const Data & d) 
  { 
    std::copy(d.bytes, d.bytes+16, this->bytes);
    std::cout << "A copy was made.\n"; 
  }
private:
  char bytes[16];
};

Data func(char c) {
  return Data(c);
}

int main(void) {
   Data d1 = func(A);
}

以下虛擬碼顯示瞭如何消除 Data 的兩個副本。

void func(Data * target, char c) 
{  
  new (target) Data (c);  // placement-new syntax (no dynamic allocation here)
  return;                 // Note void return type.
}
int main (void)
{
   char bytes[sizeof(Data)];                   // uninitialized stack-space to hold a Data object
   func(reinterpret_cast<Data *>(bytes), 'A'); // Both the copies of Data elided
   reinterpret_cast<Data *>(bytes)->~Data();   // destructor
}

命名返回值最佳化 (NRVO) 是 RVO 的更高階的表親,並非所有編譯器都支援它。請注意,上面的函式 func 沒有命名它建立的區域性物件。通常,函式比這更復雜。它們建立區域性物件,操作其狀態,然後返回更新的物件。在這些情況下,消除區域性物件需要 NRVO。請考慮以下有點牽強的示例來強調此習語的計算部分。

class File {
private: 
  std::string str_;
public:
  File() {}
  void path(const std::string & path) { 
    str_ = path;  
  }
  void name(const std::string & name)  {
    str_ += "/";
    str_ += name;
  }
  void ext(const std::string & ext) {
    str_ += ".";
    str_ += ext;
  }
};

File getfile(void) {
  File f;
  f.path("/lib");
  f.name("libc");
  f.ext("so");
  f.ext("6");

  // RVO is not applicable here because object has a name = f
  // NRVO is possible but its support is not universal.
  return f; 
}

int main (void) {
  File  f = getfile(); 
}

在上面的示例中,函式 getfile 在返回物件 f 之前對其進行大量計算。該實現無法使用 RVO,因為該物件有名稱(“f”)。NRVO 是可能的,但它的支援並不普遍。計算建構函式習語是一種即使在這些情況下也能實現返回值最佳化的方法。

解決方案和示例程式碼

[編輯 | 編輯原始碼]

為了利用 RVO,計算建構函式習語背後的想法是將計算放在建構函式中,以便編譯器更有可能執行最佳化。添加了一個新的四個引數建構函式,僅為了啟用 RVO,它就是類 File計算建構函式。現在 getfile 函式比以前簡單得多,並且編譯器可能會在此應用 RVO。

class File 
{
private: 
  std::string str_;
public:
  File() {}
  
  // The following constructor is a computational constructor
  File(const std::string & path, 
       const std::string & name,
       const std::string & ext1,
       const std::string & ext2) 
    : str_(path + "/" + name + "." + ext1 + "." + ext2) { }

  void path(const std::string & path);
  void name(const std::string & name);
  void ext(const std::string & ext);
};

File getfile(void) {
  return File("/lib", "libc", "so", "6"); // RVO is now applicable 
}

int main (void) {
  File  f = getfile(); 
}

對計算建構函式習語的一個常見批評是,它會導致不自然的建構函式,這在上面顯示的類 File 中部分屬實。如果謹慎地應用此習語,它可以限制類中計算建構函式的激增,同時仍然提供更好的執行時效能。

已知用途

[編輯 | 編輯原始碼]
[編輯 | 編輯原始碼]
  • Dov Bulka,David Mayhew,“高效 C++;效能程式設計技術”,Addison Wesley
華夏公益教科書