What happens to an object instance after applying std::move [duplicate]
Asked Answered
B

6

9

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.

Bookseller answered 21/6, 2015 at 10:35 Comment(1)
in order to truly make this construction as "move" you need to null b.a , else you only preforming shallow copying.Rackrent
T
8

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.

Tevere answered 21/6, 2015 at 10:55 Comment(11)
I am sorry. I could not fully understand the example. Forgive my inexperience. What exactly is remove_reference::type ?Bookseller
Just to add to this. Object like vectors allocate internal storage so when one creates an object based on an expiring object, there is no need to copy the internal storage, all one need to do is tranfer ownership of the internal storage.Roselynroseman
Thanks doron. I actually feel like that is the major use case of std::move and rvalue references. Because naturally, and generally, when we fill something like a vector, we just want the vector, not the object itself.Bookseller
@OzumSafa You should look into type_traits. To simplify, the code is an elaborate way of saying to the compiler : "I know something you don't; that this object instance is as good as expired after the function, so call an overlad that takes advantage of this if there is one"Tevere
I also dont understand the part where you said T&& arg is a universal reference. Is there a way you could elaborate that or should I dig in myself ?Bookseller
By type you mean any type like string or set?Bookseller
@OzumSafa You are touching too many topics for a single question. I hope this helps, though you'd have to read it by googling unkonown terms. type is a nested type in remove_reference and since this is generic code it could be any type. Finally this is a great intro lecture on the topicTevere
You are right, I tend to do that :) . Could you check my edit on the question ?Bookseller
So only reason to use std::move is to be able to choose the appropriate overloaded constructor ( or method ) ??Bookseller
@OzumSafa Yes. That's not a modest use and in some cases it's the only way to use move semantics.Tevere
I didnt mean it like "lol that is bad" :). I guess my mind just imagined something different. Well now it is clear. Thank you for your time.Bookseller
H
3

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
Habituate answered 21/6, 2015 at 13:11 Comment(3)
@HanLuo, the code does compile. The output I provided is a copy. Make sure to have the right C++ environment enabled (tested with C++ 11). Also: Simply claiming that the code doesn't compile is not very constructive. What's the compilation error? In case you have trouble figuring out the correct project setup, consider asking a question here on Stack Overflow. That's what this site is for.Habituate
No offense here. You can just copy your code to cpp.sh. The code won't be compiled with the setting "C++11 Many (-Wall) Full (-O2)". You can also directly view this one onlinegdb.com/V6oPTd5CQDapper
@HanLuo, I took a look at the errors (thanks for pointing them out) and corrected them. I forgot which compiler and project setup (apart from C++11) I used the first time - maybe MSVC, but it really could've been anything. Should be better now. In my opinion the new version is much cleaner (not perfect, it never is) since it doesn't use raw pointers anymore.Habituate
W
0

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.

Walkerwalkietalkie answered 21/6, 2015 at 10:38 Comment(0)
A
0

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).

Antiar answered 21/6, 2015 at 10:43 Comment(2)
That's if you write the move constructor and it is your responsibility; for standard classes like std::vector the library has done it for you. And it's just the destructor that needs to survive and nothing more; calling any other method is undefined behaviour.Tajuanatak
He wrote the move constructor; that's why my second paragraph is here. I removed a portion of the last sentence to reflect your reference to UB.Antiar
R
0

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.

Roselynroseman answered 21/6, 2015 at 11:32 Comment(2)
I am familiar with the concept of rvalue. So you are trying to escape from copying and creating a new object. I am curious what happens to the old object, aka the owner of the resource of the rvalue str . What kind of a state is it left in ?Bookseller
I don't think it is safe to make any assumptions about the old object. Maybe some standards guru can give you a more definitive answer.Roselynroseman
D
0

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.

Dapper answered 30/12, 2020 at 11:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.