C++ object as return value: copy or reference?

20,022

Solution 1

You have violated the Rule of Three.

Specifically, when you return an object, a copy is made, and then destroyed. So, you have a sequence of events like

Ctest1::Ctest1(void);
Ctest1::Ctest1(const Ctest1&);
Ctest1::~Ctest1();
Ctest1::~Ctest1();

That is two objects are created: your original object construction, followed by the implicit copy constructor. Then both of those objects are deleted.

Since both of those objects contain the same pointer, you end up calling delete twice on the same value. BOOM


Extra Credit: When I investigate issues like "I wonder how the copies get made", I put print statements in the interesting class methods, like this:
#include <iostream>

int serial_source = 0;
class Ctest1
{
#define X(s) (std::cout << s << ": " << serial << "\n")
  const int serial;
public:
   Ctest1(void) : serial(serial_source++) {
     X("Ctest1::Ctest1(void)");
   }
   ~Ctest1(void) {
    X("Ctest1::~Ctest1()");
   }
   Ctest1(const Ctest1& other) : serial(serial_source++) {
    X("Ctest1::Ctest1(const Ctest1&)");
    std::cout << " Copied from " << other.serial << "\n";
   }
   void operator=(const Ctest1& other) {
     X("operator=");
     std::cout << " Assigning from " << other.serial << "\n";
   }
#undef X
};

Ctest1 Function(Ctest1* cPtr){
   return *cPtr;    
}

int main()
{
   Ctest1* cPtr;

   cPtr=new Ctest1();


   for(int i=1;i<10;i++)
      *cPtr = Function(cPtr);

   delete cPtr;

   return 0;
}

Solution 2

Getting (finally) to what you originally intended to ask about, the short answer is that it's rarely a problem. The standard contains a clause that specifically exempts a compiler from having to actually use the copy constructor on a return value, even if the copy constructor has side effects, so the difference is externally visible.

Depending on whether you're returning a variable, or just a value, this is called either named return value optimization (NRVO) or just return value optimization (RVO). Most reasonably modern compilers implement both (some, such as g++ even do it when you turn off optimization).

To avoid copying the return value, what the compiler does is pass the address where the copy would go as a hidden parameter to the function. The function then constructs its return value in that place, so after the function returns, the value is already there without being copied.

This is common enough, and works well enough that Dave Abrahams (at the time a C++ standard committee member) wrote an article some years ago showing that with modern compilers, people's attempts at avoiding the extra copying often actually produce code that's slower than if you just write simple, obvious code.

Solution 3

As Rob said, you haven't created all three constructor/assignment operators that C++ uses. What the Rule of Three he mentioned means is that if you declare a Destructor, Copy Constructor, or Assignment Operator (operator=()), you need to use all three.

If you don't create these functions, then the compiler will create its own version of them for you. However, the compilers copy constructor and assignment operators only do a shallow copy of elements from the original object. This means that the copied object that's created as the return value, and then copied into the object in main() has a pointer to the same address as the first object you created. So when that original object is destroyed to make room for the copied object, the classSpace array on the heap is freed, causing the copied object's pointer to become invalidated.

Solution 4

If you want to see when a copy of an object is made just do this:

struct Foo {
    Foo() { std::cout << "default ctor\n"; }
    Foo(Foo const &) { std::cout << "copy ctor\n"; }
    Foo(Foo &&) { std::cout << "move ctor\n"; }
    Foo &operator=(Foo const &) { std::cout << "copy assign\n"; return *this; }
    Foo &operator=(Foo &&) { std::cout << "move assign\n"; return *this; }
    ~Foo() { std::cout << "dtor\n"; }
};

Foo Function(Foo* f){
   return *f;    
}

int main(int argc,const char *argv[])
{
   Foo* f=new Foo;

   for(int i=1;i<10;i++)
      *f = Function(f);

   delete f;
}
Share:
20,022
user1316208
Author by

user1316208

Updated on August 21, 2021

Comments

  • user1316208
    user1316208 over 2 years

    I wanted to test how C++ behaves when the return value of a function is an object. I made this little example to watch how many bytes are allocated and determine whether the compiler makes a copy of object (like when object is passed as a parameter) or instead returns some kind of reference.

    However, I couldn't run this very simple program and I have no idea why. Error says: "Debug assertion failed! Expression: BLOCK_TYPE_IS_INVALID" in some dbgdel.cpp file. Project is a win32 console application. But I'm pretty sure that there is something wrong with this code.

    class Ctest1
    {
    public:
       Ctest1(void);
       ~Ctest1(void);
    
       char* classSpace;
    };
    
    Ctest1::Ctest1(void)
    {
       classSpace = new char[100];
    }
    
    Ctest1::~Ctest1(void)
    {
       delete [] classSpace;
    }
    
    Ctest1 Function(Ctest1* cPtr){
       return *cPtr;    
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
       Ctest1* cPtr;
    
       cPtr=new Ctest1();
    
    
       for(int i=1;i<10;i++)
          *cPtr = Function(cPtr);
    
    
       delete cPtr;
    
       return 0;
       }