Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zombie objects after std::move

I'm confused about the state of an object after it's been moved using C++0x move semantics. My understanding is that once an object has been moved, it's still a valid object, but its internal state has been altered so that when its destructor is called, no resources are deallocated.

But if my understanding is correct, the destructor of a moved object should still be called.

But, that doesn't happen when I perform a simple test:

struct Foo
{
    Foo()  
    {
        s = new char[100]; 
        cout << "Constructor called!" << endl;  
    }

    Foo(Foo&& f) 
    {
        s = f.s;
        f.s = 0;
    }

    ~Foo() 
    { 
        cout << "Destructor called!" << endl;   
        delete[] s; // okay if s is NULL
    }

    void dosomething() { cout << "Doing something..." << endl; }

    char* s;
};

void work(Foo&& f2)
{
    f2.dosomething();
}

int main()
{
    Foo f1;
    work(std::move(f1));
}

This output:

Constructor called!
Doing something...
Destructor called!

Notice the destructor is only called once. This shows that my understanding here is off. Why wasn't the destructor called twice? Here's my interpretation of what should have happened:

  1. Foo f1 is constructed.
  2. Foo f1 is passed to work, which takes an rvalue f2.
  3. The move constructor of Foo is called, moving all resources in f1 to f2.
  4. Now f2's destructor is called, releasing all resources.
  5. Now f1's destructor is called, which doesn't actually do anything since all resources were transferred to f2. Still, the destructor is called nonetheless.

But since only one destructor is called, either step 4 or step 5 isn't happening. I did a backtrace from the destructor to see where it was being invoked from, and it's being invoked from step 5. So why isn't f2's destructor also called?

EDIT: Okay, I modified this so it's actually managing a resource. (An internal memory buffer.) Still, I get the same behavior where the destructor is only called once.

like image 849
Channel72 Avatar asked Nov 02 '10 21:11

Channel72


2 Answers

Edit (New and correct answer)
Sorry, looking closer at the code, it seems the answer is much simpler: you never invoke the move constructor. You never actually move the object. You just pass a rvalue reference to the work function, which calls a member function on that reference, which still points back to the original object.

Original answer, saved for posterity

In order to actually perform the move, you have to have something like Foo f3(std::move(f2)); inside work. Then you can call your member function on f3 which is a new object, created by moving from f

As far as I can see, you don't get move semantics at all. You're just seeing plain old copy elision.

for the move to happen, you have to use std::move (or specifically, the argument being passed to the constructor has to be an unnamed/temporary) rvalue reference, such as the one that is returned from std::move). Otherwise it is treated as a plain old-fashioned lvalue reference, and then a copy should happen, but as usual, the compiler is allowed to optimize it away, leaving you with one object being constructed, and one object being destroyed.

Anyway, even with move semantics, there's no reason why the compiler shouldn't do the same thing: just optimize the move, just like it'd have optimized away the copy. A move is cheap, but it's still cheaper to just construct the object where you need it, rather than constructing one, and then moving it into another location and calling the destructor on the first one.

It's also worth noting that you're using a relatively old compiler, and earlier versions of the spec were very unclear on what should happen with these "zombie objects". So it is possible that GCC 4.3 just doesn't call the destructor. I believe it's only the last revision, or maybe the one before it, that explicitly requires the destructor to be called

like image 108
jalf Avatar answered Nov 09 '22 01:11

jalf


Please note that the compiler may optimize out the unneeded construction/destruction, effectively making only one object exist. This is especially true for rvalue references (which were invented exactly for this purpose).

I think you are wrong in your interpretation of what happens. The move constructor is not called: if the value is not a temporary, the rvalue reference behaves as a normal reference.

Maybe this article will bring more information about rvalue reference semantics.

like image 28
Vlad Avatar answered Nov 08 '22 23:11

Vlad