More C++ Idioms/Computational Constructor

Computational ConstructorEdit

IntentEdit

  • Optimize return-by-value
  • Allow Return Value Optimization (RVO) on compilers that cannot handle Named Return Value Optimization (NRVO)

Also Known AsEdit

MotivationEdit

Returning large C++ objects by value is expensive in C++. When a locally created object is returned by-value from a function, a temporary object is created on the stack. The temporaries are often very short-lived because they are either assigned to other objects or passed to other functions. Temporary objects generally go out-of-scope and hence destroyed after the statement that created them is executed completely.

Over the years compilers have evolved to apply several optimizations to avoid creation of temporaries because it is often wasteful and hampers performance. Return Value Optimization (RVO) and Named Return Value Optimization (NRVO) are two popular compiler techniques that try to optimize away the temporaries (a.k.a. copy elision). A brief explanation of RVO is in order.

Return Value Optimization

The following example demonstrates a scenario where the implementation may eliminate one or both of the copies being made, even if the copy constructor has a visible side effect (printing text). The first copy that may be eliminated is the one where Data(c) is copied into the function func's return value. The second copy that may be eliminated is the copy of the temporary object returned by func to d1. More on RVO is available on Wikipedia

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’);
}

Following pseudo-code shows how both the copies of Data can be eliminated.

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
}

Named Return Value Optimization (NRVO) is a more advanced cousin of RVO and not all compilers support it. Note that function func above did not name the local object it created. Often functions are more complicated than that. They create local objects, manipulate its state, and return the updated object. Eliminating the local object in such cases requires NRVO. Consider the following somewhat contrived example to emphasize the computational part of this idiom.

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(); 
}

In the above example, function getfile does a lot of computation on object f before returning it. The implementation cannot use RVO because the object has a name ("f"). NRVO is possible but its support is not universal. Computational constructor idiom is a way to achieve return value optimization even in such cases.

Solution and Sample CodeEdit

To exploit RVO, the idea behind computational constructor idiom is to put the computation in a constructor so that the compiler is more likely to perform the optimization. A new four parameter constructor has been added just to enable RVO, which is a computational constructor for class File. The getfile function is now much more simple than before and the compiler will likely apply RVO here.

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(); 
}

ConsequencesEdit

A common criticism against the computational constructor idiom is that it leads to unnatural constructors, which is partly true for the class File shown above. If the idiom is applied judiciously, it can limit the proliferation of computational constructors in a class and yet provide better run-time performance.

Known UsesEdit

Related IdiomsEdit

ReferencesEdit

  • Dov Bulka, David Mayhew, “Efficient C++; Performance Programming Techniques”, Addison Wesley
Last modified on 20 February 2012, at 15:36