Do Built-In Types Have Move Semantics

Do built-in types have move semantics?

And so, is the one shown by the example a well-defined behavior?

Yes, the behaviour shown in the example is the only behaviour allowed by the standard. That is because std::move doesn't move. The things that move are move constructors and move assignment operators.

All std::move does is change an lvalue into an xvalue, so that it can bind to rvalue references. It does not invoke any constructor or anything else. Changing the value category happens at the type level. Nothing happens at runtime.

Rvalue references are still references: they refer to the original object. The function increments the original integer through the reference given.

If a function takes an argument by reference, no copies nor moves happen: the original object is bound to the reference.

If a function takes an argument by value, then we might have a move.

However, fundamental types don't have move constructors. Moves degrade to copies in that case.

C++ move semantics and built-in types

It is not possible and/or doesn't make sense.


We start with:

A original; // original object

And the main part of the question is:

What do I do if I want to hijack (not copy) an instance of the class
such as this one?

So that would mean that we end up with:

A movedto; // new object that has all of original's members

But the caveat here is that we want "to circumvent the copying" and we don't want to use references or pointers, i.e "reference semantics", only the "stack" or "value semantics".

If we want movedto to have the same members at the same memory locations that were already allocated then we can just create a reference to original:

A& movedto{original}; // references members at the same memory locations. 

But part of this question states that we are not using references because presumably we want this object to have a different lifetime. So if we want to keep original's members "alive" and allocated beyond the end of the current block then we immediately find that we are not in control of that underlying memory.


In this question original is an object with automatic storage durtion. Objects with automatic storage duration have their lifetimes managed automatically according to their scope. The compiler may have used a stack to store it and the compiler may use a stack pointer that gets moved downwards each time an object is added but the C++ standard doesn't specify how it should be done. We do know that the standard specifies that the objects with automatic storage duration will be destroyed in the reverse order they were created when the scope ends.

So trying to control where an object with automatic storage durtaion is created does not make sense and assigning the members of such an object to another doesn't make sense either. The memory is allocated automatically.

If we want to reuse the variables that were already allocated as part of an object with automatic storage duration (stack/value semantics) then we're using memory that will be deallocated when that object's lifetime ends. We must use dynamic storage for that (i.e. the "heap", or "reference semantics").

What is move semantics?

I find it easiest to understand move semantics with example code. Let's start with a very simple string class which only holds a pointer to a heap-allocated block of memory:

#include <cstring>
#include <algorithm>

class string
{
char* data;

public:

string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}

Since we chose to manage the memory ourselves, we need to follow the rule of three. I am going to defer writing the assignment operator and only implement the destructor and the copy constructor for now:

    ~string()
{
delete[] data;
}

string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}

The copy constructor defines what it means to copy string objects. The parameter const string& that binds to all expressions of type string which allows you to make copies in the following examples:

string a(x);                                    // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3

Now comes the key insight into move semantics. Note that only in the first line where we copy x is this deep copy really necessary, because we might want to inspect x later and would be very surprised if x had changed somehow. Did you notice how I just said x three times (four times if you include this sentence) and meant the exact same object every time? We call expressions such as x "lvalues".

The arguments in lines 2 and 3 are not lvalues, but rvalues, because the underlying string objects have no names, so the client has no way to inspect them again at a later point in time.
rvalues denote temporary objects which are destroyed at the next semicolon (to be more precise: at the end of the full-expression that lexically contains the rvalue). This is important because during the initialization of b and c, we could do whatever we wanted with the source string, and the client couldn't tell a difference!

C++0x introduces a new mechanism called "rvalue reference" which, among other things,
allows us to detect rvalue arguments via function overloading. All we have to do is write a constructor with an rvalue reference parameter. Inside that constructor we can do anything we want with the source, as long as we leave it in some valid state:

    string(string&& that)   // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}

What have we done here? Instead of deeply copying the heap data, we have just copied the pointer and then set the original pointer to null (to prevent 'delete[]' from source object's destructor from releasing our 'just stolen data'). In effect, we have "stolen" the data that originally belonged to the source string. Again, the key insight is that under no circumstance could the client detect that the source had been modified. Since we don't really do a copy here, we call this constructor a "move constructor". Its job is to move resources from one object to another instead of copying them.

Congratulations, you now understand the basics of move semantics! Let's continue by implementing the assignment operator. If you're unfamiliar with the copy and swap idiom, learn it and come back, because it's an awesome C++ idiom related to exception safety.

    string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};

Huh, that's it? "Where's the rvalue reference?" you might ask. "We don't need it here!" is my answer :)

Note that we pass the parameter that by value, so that has to be initialized just like any other string object. Exactly how is that going to be initialized? In the olden days of C++98, the answer would have been "by the copy constructor". In C++0x, the compiler chooses between the copy constructor and the move constructor based on whether the argument to the assignment operator is an lvalue or an rvalue.

So if you say a = b, the copy constructor will initialize that (because the expression b is an lvalue), and the assignment operator swaps the contents with a freshly created, deep copy. That is the very definition of the copy and swap idiom -- make a copy, swap the contents with the copy, and then get rid of the copy by leaving the scope. Nothing new here.

But if you say a = x + y, the move constructor will initialize that (because the expression x + y is an rvalue), so there is no deep copy involved, only an efficient move.
that is still an independent object from the argument, but its construction was trivial,
since the heap data didn't have to be copied, just moved. It wasn't necessary to copy it because x + y is an rvalue, and again, it is okay to move from string objects denoted by rvalues.

To summarize, the copy constructor makes a deep copy, because the source must remain untouched.
The move constructor, on the other hand, can just copy the pointer and then set the pointer in the source to null. It is okay to "nullify" the source object in this manner, because the client has no way of inspecting the object again.

I hope this example got the main point across. There is a lot more to rvalue references and move semantics which I intentionally left out to keep it simple. If you want more details please see my supplementary answer.

Move semantics and primitive types

  1. No, if you want to be able to assume this you should copy, not move.

  2. A move leaves the source object in a valid but unspecified state. Primitive types do exhibit move semantics. The fact that source object is left unchanged is what suggests that copying is the fastest way to implement moving.

Does move constructor change the memory to which this points to?

will the implicitly synthesized move constructor just make this in y point to the memory that x is in?

The synthesized move ctor will memberwise-move the data members of its arguments(x here) to the object being created(y here).

Also, note that for built-in types like int, move is the same as copying.



how to ensure that x is destructible after the move

Since move is the same as copying for built in types, x and y are independent of each other. So when x is destroyed y will not be affected.



will the synthesized move constructor set the ptr of x to nullptr?

No, the synthesize move ctor will not do that for you. It will just memberwise move the data members. For built in types this means the same as copying.



if it does not do this, then the ptr of x will still point to the memory to which the ptr of y points, then you cannot safely destroy x.

In that case, you'd need to write a user-defined move ctor which explicitly sets the ptr of x to nullptr so that when x is destroyed, the y is not affected.

copy vs std::move for ints

In this example, there is no difference. We will end up with 3 ints with value 100. There could definitely be a difference with different types though. For instance, let's consider something like vector<int>:

std::vector<int> a = {1, 2, 3, 4, 5}; // a has size 5
auto a_copy = a; // copy a. now we have two vectors of size 5
auto a_move = std::move(a); // *move* a into a_move

The last variable, a_move, takes ownership of a's internal pointers. So what we end up with is a_move is a vector of size 5, but a is now empty. The move is much more efficient than a copy (imagine if it was a vector of 1000 strings instead - a_copy would involve allocating a 1000-string buffer and copying 1000 strings, but a_move just assigns a couple pointers).

For some other types, one might be invalid:

std::unique_ptr<int> a{new int 42};
auto a_copy = a; // error
auto a_move = std::move(a); // OK, now a_move owns 42, but a points to nothing

For many types, there's no difference though:

std::array<int, 100> a;
auto a_copy = a; // copy 100 ints
auto a_move = std::move(a); // also copy 100 ints, no special move ctor

More generally:

T a;
auto a_copy = a; // calls T(const T& ), the copy constructor
auto a_move = std::move(a); // calls T(T&& ), the move constructor

c++ move semantics does not perform move

The data member m_street and m_city are declared as const; then in the member initializer list of move constructor of Address like m_street {std::move(other.m_street)}, the copy constructor (but not move constructor) of std::string is used.

You might want to remove the const qualifier, then you'll get

Contact: John Doe
Address: (street: 123 East Dr, city: London, suite: 123)

Address: (street: , city: , suite: 123)

LIVE

BTW: For built-in types like int, the effect of move is same as copy. That's why the value of suite is still 123.

BTW2: For the move constructor of std::string,

Move constructor. Constructs the string with the contents of other using move semantics. other is left in valid, but unspecified state.

After move operation the moved object doesn't guarantee to be modified to empty.

Understanding Move Semantics of C++ Standard

When we move an object with by std::move

No. std::move doesn't move things. It gives us an rvalue expression referring to some object. An rvalue expression results in overload resolution picking functions that take rvalue references, which is convenient because now we will pick those instead of the functions that take values, or lvalue references, which traditionally do copy-like things. Such functions tend to be constructors (so-called "move constructors") or assignment operators (so-called "move assignment operators") because it is during construction and assignment that moving is useful.

it makes the object nullptr

That depends entirely on what said function does. Typically, an object that's worth moving has indirect state, like a pointer to some dynamically-allocated resource. And if said object is moveable, its move constructor is best off doing a swap on those pointers. If it didn't leave the source object's pointer as nullptr then you have two objects owning the resource. That wasn't a move; it was a [shallow] copy.

A full discussion of the hows and the whys is out of scope of a Q&A, but any good book should have one.

Also, if this all sounds like a hack, that's because it is one. C++ is a hodge-podge of hacks built on hacks to provide additional functionality over time. In this case, we needed (read: wanted) a way to create a different kind of constructor (and assignment operator), a kind that would take a non-const reference and could modify the source object (to "steal" its resources), and to achieve that we had to introduce a new kind of reference that would only bind to rvalues (because we were already using everything else for copies), and then we had to introduce a utility function that would turn the name of an object into an rvalue … hence, std::move was born, the utility function that doesn't move anything. /p>

C++11 - Moving fundamental data types in constructor?

Initialization and assignment of a primitive from a prvalue or xvalue primitive has exactly the same effect as initialization or assignment from a lvalue primitive; the value is copied and the source object is unaffected.

In other words, you can use std::move but it won't make any difference.

If you want to change the value of the source object (to 0, say) you'll have to do that yourself.



Related Topics



Leave a reply



Submit