What happens to an object instance after applying std::move

8k views Asked by At

I am trying to understand how std::move and rvalues work in C++ 11. I keep seeing the similar example in tutorials like this:

Suppose we have this class :

class Class 
{
   public:
      Class(Class &&b)
      {
          a = b.a;
      }
      int *a;
}

int main()
{
    Class object1(5);
    Class object2(std::move(object1));
}

After the second line in main function is ran, what happens to object1? If memory of object1 is "moved" to object2, what is the point of this copy constructor ? As we are losing memory of object1 just to get exact same value in a different place in memory? What is the use case of this?

Edit : Question is not a duplicate. The candidate for duplicative is much broader in the sense that it does not even have a code snippet.

Edit2 : I have just tried this piece of code :

class trial
{
public:
    trial()
    {
        a = (int*)malloc(4);
    }

    trial(trial& rv)
    {
        this->a = rv.a;
        rv.a = NULL;
    }

    int *a;
};

int main() {
    cout << "Program Started" << endl;
    trial a1;
    trial a2(a1);


    return 0;
}

And I have checked the internals of a1 and a2. It gives the exact same result with this code :

class trial
{
public:
    trial()
    {
        a = (int*)malloc(4);
    }

    trial(trial&& rv)
    {
        this->a = rv.a;
        rv.a = NULL;
    }

    int *a;
};

int main() {
    cout << "Program Started" << endl;
    trial a1;
    trial a2(std::move(a1));


    return 0;
}

The difference is with the copy constructor, one of which does not use move semantics. Just plain reference for the object. No copying occurs if we also pass by reference, but we are going to have to lose the first object somewhat by doing rv.a = NULL, to avoid accidental memory freeing for a2, by freeing a1. So I set the rv.a = NULL.

When I use rvalue copy constructor of class trial, but do not use the line rv.a = NULL in the rvalue constructor, the integer pointer a shows the same address both in a1 and a2 when i put a break point at the line return 0. So how is this different than just passing by reference? It looks like we can do exactly the same by passing by reference.

6

There are 6 answers

11
Nikos Athanasiou On BEST ANSWER

Nothing.

std::move does not move a thing. It simply casts (converts) the object to an rvalue reference, which can be seen by looking at a typical implementation :

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

note that the T&& arg is a universal reference in a deducable context and not an rvalue reference per se (in case you were wondering "isn't arg an rvalue ref already?")

It's the functions that use rvalue refs, like move constructors and move assignment operators or regular functions with && args, that can take advantage of this value category (it's called xvalue ie expiring objects) and avoid overheads by moving data out of the object, leaving it in a valid but unspecified state (eg destructible).

As per EDIT 2

I think you answer your own question. Imagine you had both constructors, move and copy, in the class; what std::move does is let you select the first one when calling

trial a2(std::move(a1));

since your implementation for both is the same, they're going to do the same thing. A typical implementation would avoid aliasing in the copy constructor case :

trial(trial& rv)
{
    this->a = (int*)malloc(sizeof(int));
    this->a = rv.a;
}

which means an extra allocation needs to be performed (you just want a copy, why messing with the original?).

When calling the move costructor on the other hand, you're basically telling the compiler "hey I'm not gona use a1 anymore, do your best" and your move construction is called and you "transplant" a1 resources to a2.

3
Brian On

What happens to an object instance after applying std::move?"

Nothing. It'll be treated as any other object after that. This means that the destructor will still be called. As rems4e already mentioned, you should transfer the state (e.g. by copying pointers, because that's cheap) and leave the original object with no references to it's former resources (if the destructor tries to free them as it should) or some other defined state.

"After the second line in main function is ran, what happens to object1?"

You hit a scope exit }and this triggers a destructor call. First on object2, then on object1.

If memory of object1 is "moved" to object2, what is the point of this copy constructor? As we are losing memory of object1 just to get exact same value in a different place in memory? What is the use case of this?

Think of it as a specialization. While the real copy constructor enables you to duplicate an object (deep down to its leafs, e.g. when doing an assignment of object1 to object2) which could be very, very expensive, the move constructor enables you to transfer a state quickly by just copying the pointers of its members. This comes in handy when returning from a function.

Here's an example:

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Person {
private:
    shared_ptr<string> name;
public:
    Person(shared_ptr<string> name) {
        cout << "Constructing " << *name << endl;
        this->name = name;
    }
    Person(const Person& original) {
        cout << "Copying " << *original.name << endl;
        name = make_shared<string>("Copy of " + *original.name);
    }
    Person(Person&& original) {
        cout << "Moving " << *original.name << endl;
        name = make_shared<string>(*original.name + ", the moved one");
        original.name = make_shared<string>("nobody (was " + *original.name + ")");
    }
    ~Person() {
        cout << "Destroying " << *name << endl;
        name = make_shared<string>();
    }
};

Person give_it_here(shared_ptr<string> name) {
    return Person(name);
}

int main(int argc, char* argv[]) {
    {
        Person p1(make_shared<string>("John"));
        Person p2 = move(p1); // Unnecessarily moving to another variable. It makes no sense.
    }
    cout << endl;

    {
        Person p1(make_shared<string>("James"));
        Person p2 = p1; // Copying here. Could make sense, but it depends.
    }
    cout << endl;

    {
        Person p1 = give_it_here(make_shared<string>("Jack")); // Let some other function create the object and return (move) it to us.
    }

    return 0;
}

The code prints (using g++ with C++11 and -fno-elide-constructors)

Constructing John
Moving John
Destroying John, the moved one
Destroying nobody (was John)

Constructing James
Copying James
Destroying Copy of James
Destroying James

Constructing Jack
Moving Jack
Destroying nobody (was Jack)
Moving Jack, the moved one
Destroying nobody (was Jack, the moved one)
Destroying Jack, the moved one, the moved one

Remarks:

  • That flag -fno-elide-constructors is required to prevent return value optimzation (for this example)
  • For some reason that eludes me g++ generates two moves instead of one in the last example
2
doron On

std::move returns an rvalue reference based on the supplied object. This newly created rvalue reference can now be passed to functions that take an rvalue reference as a parameter like void foo (Obj&& rvalue_reference).

Without going into full details of what an rvalue reference is, all you really need to know is that rvalues are expected to expire immediately and will no longer be used again. This allows the library writer to make certain optimizing assumptions that they would not be able to make otherwise.

For example:

lets take a function like:

std::string addFullStopToEnd(std::string& str)

In the above function we would have to create a brand new string to return since our happy user may still want to use the original string.

In the function:

std::string addFullStopToEnd(std::string&& str)

We can just take the internal storage of str are append the Full Stop and return. This is safe because rvalue reference are temporary objects so there is no need to preserve them for later use.

So from a practical point of view, if one modifies a function parameter with std::move one is declaring that one will never refer to that parameter once it returns from the function. Doing so will lead to undefined behaviour.

For more info on rvalue references take a look here.

0
Steephen On

std::move will enable ownership transfer of resources including memory from object1 to object2 by converting the type of lvalue object to rvalue reference. So your move copy constructor or move assigment operator can initiate the resource transfer. So what were the resources of obj1 will be now resources of object2, if object1 is movable, in your case it is.

2
rems4e On

The typical use case is to avoid copying a large resource (like a member std::vector) for example. Without move semantics, it would have been copied. With it, the move is basically a pointer swap, which can be a lot faster.

Be careful that an object which has been moved to another one must still be in a valid state, as its destructor will still be called. So in your example, you better set the a member to nullptr, as it must not own the resource anymore (to avoid a double delete).

0
mach6 On

Be careful with the definition of your move constructor, for the following example

#include <iostream>
#include <vector>
using namespace std;

class A{

public:
    int *ptr;

  A(int x){
    // Default constructor
    cout << "Calling Default constructor\n";
    ptr = new int ;
    *ptr = x;
  }

  A( const A & obj){
    // Copy Constructor
    // copy of object is created
    this->ptr = new int;
    // Deep copying
    cout << "Calling Copy constructor\n";
  }

  A ( A && obj){
    // Move constructor
    // It will simply shift the resources,
    // without creating a copy.
     cout << "Calling Move constructor\n";
    this->ptr = obj.ptr;
    obj.ptr = NULL;
  }

  ~A(){
    // Destructor
    cout << "Calling Destructor\n";
    delete ptr;
  }

};

int main() {

  A b = A(2);
  {
      vector <A> vec;

  vec.push_back(std::move(b));
  }
 cout << *(b.ptr);  //Segmentation fault (core dumped)
  return 0;

}

Segmentation fault occurs because the memory of b.ptr is freed after the scope of vector vec.