Move Semantics
Transferring ownership of a resource instead of copying it.
Intermediate
int x = 5; // x is an lvalue — it has a name and an address
"hello"; // still an lvalue — string literals have static storage
x + 2; // rvalue — a temporary computed value
int *v = &x;
*(v + 2); // lvalue — dereferencing gives you an addressable location
v + 2; // rvalue — the pointer arithmetic itself is a temporary
int square(int &x) { return x * x; } // binds only lvalues
int square(int &&x) { return x * x; } // binds only rvalues
int square(const int &x) { return x * x; } // binds both — a const lvalue reference can bind an rvalue
T&) yields an lvalue instead, as the example below shows:
int &foo() { return globalVar; }
foo() = 5; // legal — sets globalVar to 5, because foo() returns an lvalue reference here, not a plain rvalue
std::move doesn't move anything by itself — it's a cast. It converts
its argument to an rvalue reference (static_cast<T&&>(x),
literally), which tells the compiler "I no longer need this object's current value here";
that's what makes the move constructor overload eligible instead of the copy constructor.
After the move, the moved-from object is left in a valid but unspecified state — still
safe to destroy or reassign, just not safe to assume anything about its contents.
std::move only helps when the type actually has something to move.
int, double, and other types that live entirely on the stack have
no separate heap buffer to steal — "moving" one just copies it, the same as it always
did. The optimization is specifically for types like std::string or
std::vector that own a heap allocation.
std::vector<std::string> vec1 = {"a", "b", "c"};
std::vector<std::string> vec2;
vec2 = vec1; // deep copy — every string is duplicated
vec2 = std::move(vec1); // vec2 now owns vec1's internal buffer directly; vec1 is left empty
Animal a; // default ctor
Animal b("dog", "bark"); // parameterized ctor
Animal c(b); // copy ctor
Animal d = b; // ALSO copy ctor — "Animal d = b" is sugar for "Animal d(b)", not an assignment
Animal e;
e = b; // default ctor for e, then copy assignment
Animal f(std::move(b)); // move ctor — b is left in a valid-but-empty state after this
operator= exception-safely and support both copy
and move assignment with one function: take the parameter by value (which invokes
either the copy or move constructor, whichever applies at the call site), then swap its
internals with *this. The temporary parameter is destroyed at the end of the
function, taking the old resource with it.
class Buffer {
int *data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]()), size(n) {}
Buffer(const Buffer &other) : data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
Buffer(Buffer &&other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // leave other in a valid, destructible (empty) state
other.size = 0;
}
~Buffer() { delete[] data; }
void swap(Buffer &other) noexcept {
std::swap(data, other.data);
std::swap(size, other.size);
}
Buffer & operator=(Buffer other) { // note: by value, not by reference
swap(other); // steal other's guts
return *this;
} // other (holding our OLD heap buffer) is destroyed here — freeing it
};
Buffer a(4), b(8);
a = b; // b is copied into the parameter (copy ctor), then swapped in — no manual delete[]/new[] needed
a = std::move(b); // b is moved into the parameter (move ctor above), then swapped in — b is left empty
a = Buffer(16); // the temporary is constructed directly in the parameter (guaranteed copy elision,
// C++17) — no copy or move constructor call happens here at all
std::move(b)
line above actually move rather than copy: a user-declared copy constructor and destructor
(both present here) suppress the compiler's implicit generation of a move constructor, per the
rule of five below. Without one, std::move(b) would silently fall back to the
copy constructor instead of failing to compile.
operator=(T) and a separate operator=(T&&), calls like
b = std::move(a) become ambiguous — the compiler can't tell which one you
meant. Pick one strategy: either copy-and-swap alone (simpler, one function, handles both
cases via the by-value parameter) or a genuinely separate move-assignment operator
(marginally faster, more code to maintain) — not both at once.
push_back is overloaded as push_back(const T&) and
push_back(T&&) — by const reference or by rvalue reference, not by
value — so pushing a named variable copies it, but pushing a temporary moves
it. That's the practical reason to prefer vec.push_back(getData()) over
auto tmp = getData(); vec.push_back(tmp); when you don't need the intermediate
variable for anything else — the first version lets the compiler move straight from
the temporary instead of copying a named lvalue.
std::vector<std::string> names;
std::string s = "reused elsewhere too";
names.push_back(s); // copies — s still has its value afterward, and you use it below
names.push_back(getName()); // moves — getName()'s return value is a temporary (rvalue)
names.push_back(std::move(s)); // moves — you're explicitly saying you're done with s
const disables moving it, silently. There's no
const T&& overload that push_back (or any move constructor)
actually uses for mutation, so a const std::string passed to
std::move just falls back to the copy overload — it compiles, it's just
not actually moving anything.
const std::string s = "won't move";
vec.push_back(std::move(s)); // compiles fine, but this is a copy, not a move — s is const