The move semantics applies only to the data of class types, so I’ll talk about objects, and not data as I do elsewhere. An object is an instance of a class type (i.e., a piece of memory interpreted according to how a class or a structure was defined). Usually the state of the object is called the value.
The definition of the value of an object depends on the implementation of the class. Usually the value of an object is the state of its base and member objects. However, there might be some supporting data in an object (e.g., some cache data that are part of the object state) that do not have to be part of the object value.
The value of an object can be copied when the object is:
used to initialize an object,
used in an assignment expression,
passed by value as an argument to a function,
returned by value from a function,
all of which involve:
either the initialization: T t(<expr>);
,
or the assignment: t = <expr>;
.
We’re interested in the case where the source expression <expr>
is
of a class type, i.e., it has an object, which we call the source
object, or just a source. Object t
is the target object, or just a
target.
Important facts about copying:
Copying takes time when the value to copy is big.
Copying is implemented by:
the copy constructor (to initialize an object),
the copy assignment operator (to assign to an object).
The source and the target can be anywhere.
By anywhere I mean different memory locations, i.e., copying is not limited to objects on the stack or the heap only. For instance, the source can be on the stack, and the target in the fixed-size memory for static and global data. Objects should have no clue where they live.
Copying might be a problem depending on whether it’s necessary or not. It’s not a problem, when it’s necessary, e.g., when we need to modify a copy, and leave the original intact.
Copying is a problem when it’s unnecessary. Copying is unnecessary, when the source is not needed after copying. Unnecessary copying is a performance problem: the code will work alright, but we wish it was faster.
The move semantics allows for moving the value from a source expression to a target, when copying is unnecessary. It was introduced in C++11, but its need was already recognized in the 1990’s. Moving is like salvaging goods (the value) from a sinking ship (the object that soon will not be needed).
The move semantics takes effect when:
an object is initialized, or an object is assigned to,
the source expression is an rvalue (e.g., the source is a temporary),
the type of the target has the move semantics implemented.
The move semantics is implemented by:
the move constructor (to initialize an object),
the move assignment operator (to assign to an object).
There is no magic! An object is not moved bit-by-bit to some other place. The programmer knows every detail and remains in total control.
Only the value is moved. The source, and the target objects remain in memory where they are, and they will be destroyed in a normal way.
After moving, the source must be consistent, but its state can be undefined. It must be consistent, because it will be destroyed as usual.
A class can have either the copy constructor or the move constructor, both or none.
The move constructor of class T
has a single parameter of type T
&&
.
In the example below the class has both constructors defined:
#include <iostream>
using namespace std;
struct A
{
A()
{
cout << "default ctor\n";
}
// The copy constructor has a single parameter of type const A &.
A(const A &)
{
// Copy the data from object a to *this.
cout << "copy-ctor\n";
}
// The move constructor has a single parameter of type A &&.
A(A &&)
{
// Move the data from object a to *this.
cout << "move-ctor\n";
}
};
int
main()
{
A a;
// Calls the copy constructor.
A b{a};
// Only the default constructor will be called, because the move
// constructor will be elided. Compile with -fno-elide-constructors
// to see the move constructor called.
A c{A()};
// Calls the move constructor.
A d{move(a)};
}
In the implementation of the move constructor, in the initialization
list of the base and member objects, the initializing arguments should
be rvalues, so that the compiler can choose the move constructors for
the base and member objects. To this end we can use the std::move
function, as shown in the example below, where the copy constructor is
also implemented for comparison.
#include <string>
#include <utility>
struct A {};
struct B: A
{
std::string m_s;
B() {}
// The copy constructor ------------------------------------------
// The implementation of the copy constructor has to copy the base
// and member objects of the source object.
B(const B &source): A(source), m_s(source.m_s)
{
}
// Above is the default implementation which we can get with:
// B(const B &) = default;
// The move constructor ------------------------------------------
// The implementation of the move constructor has to use the
// std::move function to move the base and member objects of the
// source object, otherwise they would be copied.
B(B &&source): A(std::move(source)), m_s(std::move(source.m_s))
{
}
// Above is the default implementation which we can get with:
// B(B &&) = default;
};
int
main()
{
B b1;
B b2(b1);
B b3(std::move(b1));
B b4{B()};
}
A class can have either the copy assignment operator or the move assignment operator, both or none.
The move assignment operator of class T
has a single parameter of
type T &&
.
In the example below the class has both operators defined:
#include <iostream>
using namespace std;
struct A
{
A()
{
cout << "default ctor\n";
}
// The copy assignment operator:
// * has a single parameter of type const A &,
// * returns A &.
A &
operator=(const A &)
{
cout << "copy assign\n";
return *this;
}
// The move assignment operator:
// * has a single parameter of type A &&,
// * returns A &.
A &
operator=(A &&)
{
cout << "move assign\n";
return *this;
}
};
int
main()
{
A a, b;
// Calls the copy assignment operator.
a = b;
// Calls the move assignment operator.
a = A();
}
If a
and b
are of type T
, then expression a = b = T()
should
move the value of the temporary object T()
to b
, and then should
copy the value from b
to a
. That expression is evaluated
right-to-left, because the assignment operator has the right-to-left
associativity.
Therefore, the move assignment operator should return an lvalue
reference, and not an rvalue reference. If the move assignment
operator returned an rvalue reference, then that expression would move
the value from the temporary object T()
to b
, and then move (not
copy) the value of b
to a
.
Interestingly, because the move assignment operator returns an lvalue
reference (when it is declared as T &operator=(T &&);
), we can
initialize an lvalue reference with the return value of the operator:
T &l = T() = T();
even though T &l = T();
would fail to compile.
In the implementation of the move assignment operator, the argument
expressions for the assignment operators of the base and member
objects should be rvalues, so that the compiler can choose the move
assignment operators for the base and member objects. To this end we
can use the std::move
function, as shown in the example below, where
the copy assignment operator is also implemented for comparison.
#include <string>
#include <utility>
struct A {};
struct B: A
{
std::string m_s;
B() {}
// The copy assignment operator ------------------------------------
// The copy assignment operator has to copy the base and the member
// objects of the source object.
B & operator=(const B &source)
{
A::operator=(source);
// We can assign (as above) to the base object this way too:
// static_cast<A &>(*this) = source;
m_s = source.m_s;
return *this;
}
// Above is the default implementation which we can get with:
// B &operator=(const B &) = default;
// The move assignment operator ------------------------------------
// The implementation of the move assignment operator has to use the
// std::move function to move the base and the member objects of the
// source object, otherwise they would be copied.
B & operator=(B &&source)
{
A::operator=(std::move(source));
// We can assign (as above) to the base object this way too:
// static_cast<A &>(*this) = std::move(source);
m_s = std::move(source.m_s);
return *this;
}
// Above is the default implementation which we can get with:
// B &operator=(B &&) = default;
};
int
main()
{
B b1, b2;
b1 = b2;
b1 = std::move(b2);
b1 = B();
}
The overload resolution of a constructor or an assignment operator (i.e., whether the copy or the move version is chosen) depends on the category of the source expression, and the availability of the copy and move overloads. The same rules apply as in the overload resolution for a function overloaded with reference types.
The special member functions are:
the default constructor,
the copy constructor, the copy assignment operator,
the move constructor, the move assignment operator,
the destructor.
A special member function can be either undeclared or declared. A function can be declared:
explicitly as:
user-defined: a programmer provides the function definition,
defaulted: a programmer requests a default implementation,
deleted: a programmer declares the function as deleted,
implicitly as:
defaulted: a compiler provides a default definition without the user requesting it,
deleted: a compiler declares the function as deleted without the programmer requesting it.
When a function is declared as deleted (regardless of whether implicitly or explicitly), the function is considered in overload resolution, but when the function is chosen, an error message reports the function is deleted.
A programmer can explicitly request the default implementation of a
special member function with = default
, like this:
struct A
{
// We need to include the default constructor, because the
// definition of the argument constructor below would inhibit the
// generation of the default constructor.
A() = default;
A(int x)
{
}
};
int
main()
{
A a, b(1);
}
All base and member objects in a defaulted (regardless of whether implicitly or explicitly):
default constructor are default constructed,
copy constructor are copy initialized,
copy assignment operator are copy assigned,
move constructor are move initialized,
move assignment operator are move assigned,
destructor are destroyed.
A programmer can explicitly request a special member function be
deleted with = delete
, like this:
// This example is wierd (I haven't yet seen a destructor deleted),
// but it shows how to explicitely delete a special member function.
struct A
{
~A() = delete;
};
int
main()
{
// This compiles, because we're not deleting the object.
A *p = new A();
// A a; // Error: a local variable has to have a destructor called.
}
All special member functions are implicitly defaulted (if they are needed), but:
the default constructor will be undeclared, if any other constructor is explicitly declared,
the copy constructor and the copy assignment operator will be implicitly deleted, if the move constructor or the move assignment operator is explicitly declared (so that a programmer has to implement, if needed, the copy constructor and the copy assignment operators),
the move constructor and the move assignment operator will be undeclared, if the copy constructor, the copy assignment operator or the destructor is explicitly declared (so that the legacy code continues to work and doesn’t have the move semantics stuffed in).
These rules ensure the seamless integration of the move semantics into
the legacy and modern code. For instance, the legacy code (such as
std::pair
) that doesn’t do any special resource management (in the
copy constructor, the copy assignment operator, and the destructor),
will have the move semantics implemented by default.
A move-only type can only be moved: it cannot be copied. This is an example of a move-only type:
#include <utility>
// A move-only type. We do not have to explicitely delete the copy
// constructor, and the copy assignment operator, because they will be
// implicitely deleted, since the move constructor and the move
// assignment operator are explicitely defaulted.
struct A
{
A() = default;
A(A &&) = default;
A & operator=(A &&) = default;
};
int
main()
{
A a;
// A b(a); // Error: we cannot copy initialize.
A b(std::move(a));
// b = a; // Error: we cannot copy assign.
b = std::move(a);
}
A function parameter is initialized with the argument expression. For a parameter of a non-reference (i.e., we pass the argument by value) class type, the constructor overload resolution will depend on the category of the argument expression and the overload availability, as usual for a function overloaded with reference types.
If constructor elision (or the return value optimization) cannot be
used, then the value of the returned object will be implicitly
moved, if the returned object is destroyed when returning from the
function. The return instruction return t;
is implicitly converted
to return std::move(t);
.
Only the return expression consisting of a variable name is implicitly moved (converted from an lvalue to an rvalue), and other expressions are not.
We shouldn’t explicitly use the std::move
function (e.g., return
std::move(t);
) in the return statement whenever we can, because it
disables the constructor elision (or the RVO).
There are two cases described below in which the RVO cannot be used, but the returned value will be implicitly moved.
When we return a function parameter. A function parameter is allocated and initialized in a location on the stack that is different from the location for the return value. A function parameter is destroyed when returning from the function, so we can move the value from it to the location for the return value. The return expression is the name of the parameter only, so the implicit move can take place.
#include <iostream>
using namespace std;
struct A
{
A() = default;
A(A &&t)
{
cout << "move-ctor\n";
}
};
A foo(A a)
{
return a;
}
int
main()
{
foo(A());
}
When we return a base object of a local object. The local object is too big to be initialized in the location for the return value, which is of the size of the base object. The local object is destroyed when returning from the function, so we can move the value from its base object to the location for the return value. Only the value of the base object will be moved, which is called object slicing, because we slice off the value of the base object. The return expression is the name of the local object only, so the implicit move can take place.
#include <iostream>
struct A
{
A() = default;
A(const A &)
{
std::cout << "copy-ctor\n";
}
A(A &&)
{
std::cout << "move-ctor\n";
}
};
struct B: A {};
A foo()
{
B b;
return b;
}
int
main()
{
foo();
}
If the local object was static, the value would have to be copied, not moved.
std::swap
functionLet’s end with how it all began. Function std::swap
is the reason
for the work on the move semantics that started in the 1990’s. This
function showed that it’s more efficient to move than to copy.
Function std::swap
takes by reference two arguments, and swaps their
values. This function is implemented in the standard library, but in
the example below we also have an example implementation to show
what’s going on:
#include <utility>
struct A
{
};
void swap(A &a, A &b)
{
A tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
int
main()
{
A x, y;
swap(x, y);
}
The move semantics was introduced in C++11.
The move semantics is used when copying is unnecessary.
The move semantics is a performance boost.
Only the values of rvalues can be moved.
A compiler can ship the default implementation of the move semantics.
A programmer doesn’t have to know about the move semantics, but it will be used by a compiler anyway.
What do we need the move semantics for?
How does the move sementics work?
What’s a move-only type?
The project financed under the program of the Minister of Science and Higher Education under the name “Regional Initiative of Excellence” in the years 2019 - 2022 project number 020/RID/2018/19 the amount of financing 12,000,000 PLN.