What a value is, and why the default copy bites
Start from the thing you already trust from C: a plain `int x = 7;` is a value. It lives in some bytes, and when you write `int y = x;` the bytes are copied — now `y` is an independent 7 that knows nothing about `x`. Change one, the other is untouched. This is value semantics, and it is the default in C++ for every type: assignment and passing an argument copy the object, not share it. The whole rung you just climbed taught you that a class can hold a resource through RAII — a heap block, a file descriptor, a lock. The uncomfortable question is what "copy the object" should mean when the object owns one of those.
Picture a tiny string class that owns a heap buffer: it stores `char *data` and a `size_t len`, allocates with `new[]` in its constructor, and frees with `delete[]` in its destructor — textbook RAII. Now write `Str b = a;`. If C++ does the obvious thing and copies the bytes of the object, it copies the pointer value `data` — not the buffer it points at. You now have two `Str` objects whose `data` fields hold the same address. This is the shallow copy trap. The two objects are not independent at all; they share one heap block they both believe they own.
The bug detonates at scope exit. When `b`'s destructor runs it calls `delete[] data`, freeing the buffer. When `a`'s destructor runs it calls `delete[]` on the same address — a deterministic, guaranteed double free, which is undefined behavior. And if either object had been mutated, the other saw the change through the shared pointer. So the default member-by-member copy is exactly wrong for any class that owns a resource. The language is not broken; it simply does not know your `char *data` is an owning pointer rather than a borrowed view. You have to tell it.
Deep copy fixes correctness — and exposes a cost
The fix you reach for first is the deep copy: when you copy a `Str`, allocate a fresh buffer and copy the characters into it, so each object owns its own block. In C++ this is done by writing a copy constructor and a copy assignment operator — two of the special member functions the compiler would otherwise generate for you. The copy constructor runs `new[]` for a new buffer the same length as the source and copies the bytes across; the copy assignment operator does the same but must first free its old buffer (and guard against `a = a;`). Now `Str b = a;` produces two genuinely separate strings, each destructor frees its own block exactly once, and the double-free is gone.
Correct — but now feel the cost. Consider a function `Str make_label() { Str s("..."); ... ; return s; }`. Returning `s` by value, and the call `Str label = make_label();`, conceptually want to copy the whole buffer out of the function. Or take `push_back` on a growing vector of `Str`: when the vector reallocates, every element must be copied to the new storage, each copy meaning a fresh `new[]` and a byte-by-byte duplication. For a megabyte-sized string this is a megabyte of allocation and copying — and in the return case, the source `s` is about to be destroyed anyway. We are deep-copying a buffer only to throw the original away an instant later. That waste is the gap move semantics fills.
Move: steal the buffer instead of copying it
Here is the central idea. When the source of a copy is a temporary or is about to die, copying its buffer is pointless — we could just take the buffer. Move semantics is exactly that: instead of allocating a new block and duplicating bytes, the destination steals the source's pointer and length, then sets the source's pointer to `nullptr` and its length to 0 so the source's destructor frees nothing. No `new[]`, no byte copy — just three pointer-sized assignments. A megabyte string "moves" in a handful of instructions regardless of its size. The buffer never moved at all; only the ownership of it did.
// move constructor: take, then null out the source
Str(Str&& other) noexcept
: data(other.data), len(other.len) // steal
{
other.data = nullptr; // leave source empty
other.len = 0; // its dtor frees nothing
}How does the compiler know it is allowed to steal rather than copy? Through a second kind of reference. An ordinary `Str&` binds to a named object that lives on — a so-called lvalue. A new kind, the rvalue reference written `Str&&`, binds only to things that are temporary or explicitly marked as expendable. The compiler picks the move constructor whenever the source is an rvalue: a temporary like the return value of `make_label()`, or any value you have promised not to use again. So returning a local by value, and constructing from a temporary, automatically move — the wasteful deep copy from the last section silently becomes a cheap pointer steal, with no change to your call sites.
When you do have a named object you are finished with — say a local `s` you want to hand off without copying — you opt in with std::move. Despite the name, `std::move` moves nothing and generates no code: it is purely a cast that produces an `Str&&` from your lvalue, telling the compiler "treat this as expendable, the move constructor may steal from it." After `Str b = std::move(a);`, the bytes of `b` are what `a` used to own, and `a` is in a valid-but-empty state: you may assign to it or let it be destroyed, but you must not assume it still holds its old value. That moved-from rule is the one genuine sharp edge here, and it is worth saying plainly.
The rule of five — and the better rule of zero
Step back and count what a resource-owning class needs. To free its buffer it needs a destructor. To copy correctly it needs a copy constructor and a copy assignment operator (deep copy, no double free). To move cheaply it needs a move constructor and a move assignment operator (steal and null out). That is five special member functions, and the observation that they come as a set is the rule of five: if your class manually manages a resource and you write any one of these, you almost certainly need to think about all five, because the compiler-generated defaults for the others will be the wrong shallow-copy behavior that double-frees.
The history is worth one sentence: before C++11 there were only four such functions (destructor, copy constructor, copy assignment, plus the default constructor sometimes counted in), and the guidance was the "rule of three." C++11 added move constructor and move assignment, turning three into five. The numbers are a mnemonic, not a law; the real content is "these special members are interdependent for a resource-owning type, so design them together."
Now the punch line, which is also the real advice: the best number of special member functions to write is zero. This is the rule of zero. Instead of hand-managing a raw `char *data` and writing all five functions, hold the resource in a type that already manages itself correctly — a `std::vector<char>`, a `std::string`, or a smart pointer (the very next guide). Those library types have correct copy, move, and destruction built in. A class made only of such members needs none of the five: the compiler-generated defaults do exactly the right thing, because each member already deep-copies, moves, and frees itself. The rule of five tells you what to do when you truly must manage a resource by hand; the rule of zero tells you to arrange your code so you almost never must.
Why this is the zero-overhead bargain, and what comes next
Stand back and see what value-plus-move buys you. You get to write code in a clean style — pass objects by value, return them by value, store them in containers — as if everything were a cheap `int`, while the heap buffers underneath are never needlessly duplicated. Returning a million-byte string out of a function costs three pointer assignments, not a million-byte copy. This is the zero-overhead principle in action: a safer, value-based style that compiles down to the same pointer-shuffling you would have hand-written in C, with no garbage collector and no run-time bookkeeping. The cost was paid once, in the design of the five (or zero) special members.
Walk the lifecycle once, slowly, to lock it in:
- A `Str s("hello")` constructor runs `new[]`, so `s` owns a heap block — RAII establishes ownership at construction.
- `Str t = s;` (a copy of a named object) runs the copy constructor: a fresh `new[]` plus a byte copy, so `t` and `s` are independent.
- `Str u = make_label();` (the source is a temporary) runs the move constructor: `u` steals the temporary's pointer, no allocation, no byte copy.
- `Str v = std::move(s);` casts `s` to an rvalue, so the move constructor steals from it; afterward `s` is valid-but-empty and must not be read for its old value.
- At scope exit each destructor runs once on the block it actually owns — `nullptr` for the moved-from one — so every buffer is freed exactly once, with no double free and no leak.
Notice what every step above leans on: a smart member type that already does copy, move, and free correctly. That is precisely the next guide. std::unique_ptr and std::shared_ptr are the standard library's answer to "give me a type that owns a heap block and obeys the rule of five so I never have to." With them, the rule of zero becomes the normal case, and the manual five-function dance you just learned becomes the rare thing you reach for only when writing a low-level container of your own.