Nathan's Lucubrations; on computers, music and the outdoors

15 07 2012

Sun, 15 Jul 2012

The Copy Constructor and Return Value Optimizations

Recently, I was tracking down a crash caused by mixing memory allocation/deallocation functions, and in the course of trying to create a solution, I came across something puzzling. I was attempting to recreate the canonical pedagogical example of passing or returning by value in C++, which normally makes use of the copy-constructor. Indeed this is what most textbooks on C++ claim. Yet the following code produces unexpected output upon execution:

/*BINFMTCXX: -DSTANDALONE
 */

// For std::cout and std::endl.
#  include <iostream>

class MyClass
{
public:
  MyClass():
    m_ii(0)
  {
    std::cout << "MyClass()" << std::endl;
  }

  MyClass(const MyClass&):
    m_ii(1)
  {
    std::cout << "MyClass::MyClass(const MyClass&)" << std::endl;
  }

  int m_ii;
};

MyClass myFunc()
{
  MyClass tmp;
  return tmp;
}

int main(void)
{
  MyClass a(myFunc());
  std::cout << a.m_ii << std::endl;
}  // int main(int argc, char* argv[])

Running this, I get

MyClass()
0

Only one constructor is called and it's not the copy-constructor. Why is this? You can try all sorts of methods to "force" the compiler to call the copy constructor, but ultimately, the compiler probably knows more than you. In the end, I tried modifying the class in the function before returning it, passing a value derived at run-time to make sure that couldn't be optimized; I tried privatizing the copy constructor as it obviously wasn't being called; I even tried throwing an exception in the copy constructor. Nothing worked! Until I tried this:

MyClass myFunc(const MyClass& orig_)
{
  return orig_;
}

int main(void)
{
  MyClass orig;
  MyClass a(myFunc(orig));
  std::cout << a.m_ii << std::endl;
}  // int main(int argc, char* argv[])

The key here is that the copy constructor is required. Previously, it was obvious to the compiler that tmp wouldn't exist outside of myFunc, so quietly eliding all those constructors (one for tmp, one to copy tmp into a) and destructors was the logical thing to do. Only when the compiler couldn't get away with that did we force its hand and make it use the copy constructor.

As an aside, the flag -fno-elide-constructors will force GCC to use copy constructors for all pass by value operations. Interestingly, if you make the copy constructor private, GCC will not compile the first example, claiming that the copy constructor is required for return by value.

posted at: 21:23 | path: | permanent link to this entry

powered by blosxom