After a move took place, the object that has been moved-from is left in a valid but unspecified state. Even if in a valid state, the fact
of an object being in an unspecified state may lead to undefined behavior.
Move construction and its respective move semantics has been introduced in C++11. Moving objects becomes interesting if one wishes to get an object
into a different scope, while no longer requiring the original object. While one would previously need to make a potentially expensive copy to get an
object into another scope and then destroy the original, move constructors allow one to move objects without performing a copy. Move
constructors are typically implemented by "stealing" the resources held by another object specified as the move constructor’s parameter, rather than
making a copy. "Stealing" resources (e.g. memory) from another object is oftentimes much more efficient than making a copy and destroying the
original, and can frequently be implemented by reassigning a few pointer variables.
Move-assignment operators behave analogously, except that they are used once the object that is moved-to has already been constructed. In contrast
to copy-assignment operators, a move-assignment operator too "steals" the moved-from object’s resources without the need for making a potentially
expensive copy.
What is the potential impact?
Using an object after it has been moved-from typically leads to undefined behavior.
For programs that exercise undefined behavior, the compiler is no longer bound by the language specification. The application may crash or, even
worse, the application may appear to execute correctly while losing data or producing incorrect results.
Trying to access an object that has been moved-from frequently ends up in a null-pointer dereference, since any pointers to the resources that have
been "stolen" are set to nullptr
as part of the move construction or move assignment.
Exceptions
There are some C++ standard template library types, such as std::unique_ptr
, for which the moved-from state is fully specified.
Exemplary type with move operations
The DynamicIntArray
type defined in the following manages memory (i.e., a resource), and shall serve as an example that showcases how
move operations "steal" another object’s resources and how they differ from making copies.
While the copy constructor does make a full copy by allocating memory and then copying the other
object’s array values into the
freshly allocated memory, the move constructor only assigns the pointer to point to other
's dynamically allocated memory. It then sets
the pointer of the other
object to nullptr
to allow its correct cleanup by the destructor of the
DynamicIntArray
type.
The implementations for the copy- and move-assignment operators are similar with the main difference being that the objects have already been
constructed.
#include <algorithm> // std::copy, std::fill
#include <memory> // std::move
class DynamicIntArray {
size_t size;
int *data;
public:
explicit DynamicIntArray(size_t size, int initial_value)
: size(size), data(new int[size]) {
std::fill(data, &data[size], initial_value);
}
~DynamicIntArray() {
delete[] data;
size = 0;
}
// Copy constructor (copies object)
DynamicIntArray(DynamicIntArray const &other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, &other.data[size], data);
}
// Move constructor ("steals" data, no allocation or copy necessary)
DynamicIntArray(DynamicIntArray &&other) noexcept
: size(other.size), data(other.data) {
// Ensure that the moved-from object `other` can be safely destroyed (using
// the destructor that calls to delete[]).
other.data = nullptr;
other.size = 0;
}
//
// Copy- and move-assignment operators are invoked, if _this_ object has
// already been constructed.
//
// Copy-assignment operator (copies object)
DynamicIntArray &operator=(DynamicIntArray const &other) {
// If the number of elements are equal, we can re-use the existing memory.
if (size == other.size) {
std::copy(other.data, &other.data[other.size], data);
return *this;
}
// Otherwise, we need to clean-up and re-allocate the required amount of
// memory.
delete[] data;
data = new int[other.size];
size = other.size;
std::copy(other.data, &other.data[size], data);
return *this;
}
// Move-assignment operator ("steals" data, no allocation or copy necessary)
DynamicIntArray &operator=(DynamicIntArray &&other) noexcept {
delete[] data; // Clean-up our own data before we "steal" from `other`.
data = other.data;
size = other.size;
// Ensure that the moved-from object `other` can be safely destroyed (using
// the destructor that calls to delete[]).
other.data = nullptr;
other.size = 0;
return *this;
}
int &getValueAt(size_t idx) { return data[idx]; }
};
int main() {
DynamicIntArray a{/*size=*/128, /*initial_value=*/42};
DynamicIntArray b = a; // Copy constructor.
DynamicIntArray c = std::move(b); // Move constructor.
// Construct two more objects.
DynamicIntArray d{/*size=*/4, /*initial_value=*/0};
DynamicIntArray e{/*size=*/8, /*initial_value=*/9001};
// Use the assignment operators.
a = d; // Copy-assignment operator.
c = std::move(e); // Move-assignment operator.
int i = b.getValueAt(0); // Noncompliant: `b` has been moved-from during construction of `c`.
int j = e.getValueAt(0); // Noncompliant: `e` has been moved-from during move-assignment to `c`.
return i + j;
}