Why this matters
Every copy the last two lessons wrote does real work: allocate, then copy every element, one at a time. That work is necessary when the original object is still needed afterward. It’s wasted when the original is about to be destroyed anyway – nothing would ever notice if, instead of duplicating the array, you just took it directly. That’s the entire idea behind moving: transferring ownership of a resource instead of duplicating it, for situations where the source object’s contents don’t need to survive.
lvalues and rvalues: does this expression have a name that outlives this statement?
This is the practical distinction that decides whether a copy or a move happens, without needing the full formal
theory: an lvalue is something with a persistent identity – a named variable like a, which you could take
the address of and which still exists on the next line. An rvalue is a temporary with no name of its own –
the direct result of makeSomething(), a literal 5, anything about to be discarded the moment the current
statement ends. Buffer b = a; copies, because a is an lvalue that’s still around afterward. The worked
example’s Buffer c = std::move(a); moves – more on std::move itself below.
The move constructor: Buffer(Buffer &&other)
Buffer(Buffer &&other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
&& (not &) declares an rvalue reference – a reference that can only bind to an rvalue, precisely the
“about to be destroyed anyway” situation where stealing is safe. The body does two things, both essential: it
copies other.data’s pointer value directly (no new[], no element-by-element loop – this is the entire
performance win), and it sets other.data to nullptr (and other.size to 0) immediately afterward. That
second part isn’t optional – without it, both this->data and other.data would hold the same address, and
other’s destructor would delete[] memory this still owns the moment other goes out of scope: the exact
double-free from two lessons ago, just introduced a different way. Marking it noexcept is a promise that moving
never throws – worth doing whenever it’s true (as it is here), since standard library containers, covered in
Module 6, specifically check for this promise before deciding it’s safe to move your objects instead of copying
them internally.
An object that’s been moved from is left “empty,” not gone
After Buffer c = std::move(a);, a still exists as a real object – its destructor will still run at the end of
main – but it’s in a moved-from state: a.data is nullptr, a.size is 0. It’s safe to destroy (that’s
exactly what the nullptr guarantees – delete[] nullptr; is a well-defined no-op) and safe to assign a new
value to, but you shouldn’t rely on its old contents for anything after moving from it – they’re gone, by design.
std::move doesn’t move anything – it’s a cast
std::move(a), from <utility>, does not move a, free anything, or change a in any way by itself. It’s
purely a cast: it tells the compiler “treat this lvalue as an rvalue for overload resolution,” which is what makes
the move constructor (rather than the copy constructor) get selected at that call site. The actual moving happens
inside whichever move constructor or move assignment operator ends up called – std::move just makes that the
one chosen, for an lvalue that would otherwise always trigger a copy. Because of this, std::move(a) is really a
promise from you, the caller, that you’re done needing a’s current contents – the language doesn’t check that
promise; getting it wrong just means using a moved-from object incorrectly afterward.
Returning a local variable often moves automatically, with no std::move needed
Buffer makeBuffer() {
Buffer temp(5);
return temp;
}
temp is a local variable – an lvalue by name – but the compiler recognizes that it’s about to be destroyed the
instant makeBuffer returns, and treats this specific return as a move automatically, without you writing
std::move(temp) yourself. (Compilers are often able to skip the move entirely here too, constructing the caller’s
object directly in place – an optimization called copy elision, worth knowing exists, not worth tracing in
detail.) Writing return std::move(temp); explicitly in a case like this is actually a common anti-pattern: it
doesn’t help, and it can prevent the compiler’s own elision from kicking in.