Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 506
Ozum Safa Avatar asked Jun 21 '15 10:06

Ozum Safa


People also ask

What happens when you to std :: move?

std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t . It is exactly equivalent to a static_cast to an rvalue reference type.

Can you use an object after std :: move?

In general, it is perfectly safe to assign to an object that has been an argument to std::move . (A particular class might cause an assertion, but that would be a very odd design.)

What does std :: move () do?

std::move() is a cast that produces an rvalue-reference to an object, to enable moving from it.

What does move () do in C ++?

std::move in C++ Moves the elements in the range [first,last] into the range beginning at result. The value of the elements in the [first,last] is transferred to the elements pointed by result. After the call, the elements in the range [first,last] are left in an unspecified but valid state.


2 Answers

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.

like image 69
Nikos Athanasiou Avatar answered Nov 04 '22 22:11

Nikos Athanasiou


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
like image 39
Brian Avatar answered Nov 04 '22 22:11

Brian