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

Go To StackoverFlow.com

7

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;
   }
2012-04-05 20:27
by user1316208
possible duplicate of Debug Assertion Failed ... BLOCKTYPEISVALID (pHead NoName 2012-04-05 20:28
The problem is that when you return the value, that copies the pointer in the object. Then both copies get destroyed, but they both point at the same block of memory. When the second is destroyed, it tries to re-delete the same block of memory, which isn't allowed - Jerry Coffin 2012-04-05 20:31
I have found a nice page where it explains how to return objects in C+ - thedarkside ofthemoon 2014-07-02 14:55


13

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;
}
2012-04-05 20:30
by Robᵩ
Though you're right, this answer is going to be utterly meaningless unless the OP already knows what this means. Can you please be more descriptive - templatetypedef 2012-04-05 20:31
You caught me in the middle of crafting the answer. It is finished now - Robᵩ 2012-04-05 20:35
wow thanks guys, never heard of rule of three, i was aware that copying object with pointer could result in this but i had no idea that expression Return can call destructor of something - user1316208 2012-04-05 20:38
why does return make copy and destroys it immediately ? :] i can't use the object from return like this ? : - user1316208 2012-04-05 20:44
@user1316208 It has to create a temporary object to act as the right-hand-side of the = operator. P.s. See my recent edit, I hope it helps - Robᵩ 2012-04-05 20:51
are you a wizard - user1316208 2012-04-05 20:56
-fno-elide-constructors flag may be usefull if you dont see extra copy-ctor calls - Fredrick Gauss 2013-10-27 07:28
Thanks @FredrickGauss, this flag is what I'm looking for. Compilers secretly perform elision optimisation for us, which saves a few destructions and copy constructions. Using -fno-elide-constructors to turn off these optimizations and see when the destructions and constructions are happening - Samuel Li 2016-12-05 19:27


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.

2012-04-05 20:39
by Tyler Gill


3

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 (a C++ standard committee member) wrote an article a few years ago showing that with modern compilers, people's attempts at avoiding the 'extra copying' actually produce code that's slower than if you just write simple, obvious code.

2012-04-05 20:52
by Jerry Coffin


2

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;
}
2012-04-05 20:51
by bames53
Wow, great minds think alike. But I didn't have the move ctor or move assignment operator - Robᵩ 2012-04-05 20:53
Ads