Copying a large object (a std::vector with a million elements, for instance) means allocating new memory and copying every element — genuinely expensive. A move constructor instead just steals the source's internal pointer and resets the source to an empty state, an operation that's nearly free regardless of how much data is involved. The compiler automatically prefers moving over copying for temporaries (rvalues) that are about to be destroyed anyway. std::move() itself performs no actual moving — it's purely a cast that marks its argument as an rvalue reference, making it eligible for the move constructor to be selected, rather than the copy constructor.
Copying vs moving: a real cost difference
b's constructor copies all million elements; c's constructor just steals a's pointer, leaving a empty — identical resulting data, radically different cost.
std::vector<int> a(1000000, 42); // a million ints
std::vector<int> b = a; // COPY — allocates new memory, copies everything
std::vector<int> c = std::move(a); // MOVE — steals a's pointer, nearly free
std::cout << a.size() << "\n"; // 0 — a was moved FROM, now valid but emptyWriting a move constructor
The move constructor takes T&& (an rvalue reference), steals the source's pointer directly, and sets the source's pointer to nullptr so its destructor won't accidentally delete memory this object now owns.
class Buffer {
int* data;
size_t size;
public:
// move constructor
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // prevent other's destructor from deleting OUR memory
other.size = 0;
}
~Buffer() { delete[] data; } // safe even if data is nullptr — delete[] on nullptr is a no-op
};std::move doesn't move anything by itself
std::move is purely a cast — it doesn't move data, allocate anything, or call any function. It just tells the compiler "treat this as an rvalue," making the move constructor a candidate for overload resolution instead of the copy constructor.
std::string s = "hello";
std::string s2 = std::move(s); // std::move just casts s to std::string&&
// after this line, s is in a valid but unspecified state — don't rely on its contents