跳转到内容

更多 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
华夏公益教科书