C++, End to End
Move Semantics and Smart Pointers
Everything up to this chapter has treated copying an object as free, conceptually. It isn't. Copying a std::vector with a million elements means allocating a new buffer and copying every element into it. Move semantics exist to let you skip that copy when the source object is about to be discarded anyway.
Lvalues and rvalues
Every expression in C++ has a category, and the two you need are lvalue and rvalue. An lvalue refers to something with a name and a persistent address: a variable, a dereferenced pointer, a function returning a reference. An rvalue is a temporary: the result of x + y, a literal like 42, a function returning by value. The rule of thumb: if you can take its address with &, it's an lvalue.
This distinction exists so the compiler can tell the difference between "an object someone else still cares about" and "an object that's about to vanish anyway, so feel free to cannibalize it."
Rvalue references
T&& is an rvalue reference: a reference that only binds to rvalues (temporaries). It's what lets you write a function overload that specifically handles "this argument is disposable":
std::move does not move anything
This trips up almost everyone the first time: std::move is a cast, not an action. It doesn't move the object, allocate anything, or free anything. It just tells the compiler "treat this lvalue as an rvalue for overload resolution purposes." Whether anything is actually moved depends entirely on whether the type you call it on has a move constructor or move assignment operator that does something with that hint.
After std::move(s), s is left in a valid but unspecified state. You can assign a new value to it or destroy it, but reading its contents is asking for trouble.
Move constructors and move assignment
A move constructor takes ownership of another object's resources instead of copying them, then leaves the source in a safe, empty state. For a class that owns a heap buffer, that usually means: steal the pointer, null out the source's pointer so its destructor doesn't free memory you now own.
Mark move constructors and move assignment noexcept whenever the operation genuinely can't throw. Containers like std::vector check this: if your move constructor isn't noexcept, vector will fall back to copying during reallocation instead of moving, because it can't safely roll back a partial move if an exception happens partway through.
Why this matters for performance
Before C++11, returning a large object by value or storing it in a vector meant real copies: new allocation, full data copy, then destroying the source. Move semantics turn most of those copies into pointer swaps. This is a big part of why modern C++ code passes and returns heavy objects by value far more casually than pre-2011 code did: the compiler and the standard library quietly move instead of copy in most of the situations where it's safe to.
std::unique_ptr: the default owning smart pointer
A std::unique_ptr<T> owns exactly one object and destroys it when the pointer itself is destroyed. It cannot be copied (there can only ever be one owner), only moved. If you find yourself writing new without immediately wrapping the result in a smart pointer, stop and ask why.
Always prefer std::make_unique<T>(args...) over new T(args...) wrapped by hand. It's exception-safe (no window where the raw pointer exists but hasn't been wrapped yet) and it's one less place to typo a mismatched delete.
std::shared_ptr: shared ownership, at a real cost
A std::shared_ptr<T> allows multiple owners; the object is destroyed when the last shared_ptr pointing to it goes away. That flexibility isn't free: every shared_ptr carries a control block with an atomic reference count, copying one means an atomic increment, and destroying one means an atomic decrement plus a check. In a hot path, that overhead is real and measurable.
Reach for shared_ptr only when ownership is genuinely shared and unclear who outlives whom. If there's one clear owner, unique_ptr is both cheaper and more honest about the design.
std::weak_ptr: breaking reference cycles
Two objects holding shared_ptrs to each other will never reach a reference count of zero: a cycle. std::weak_ptr observes an object managed by shared_ptr without contributing to its reference count, which breaks the cycle. You can't dereference a weak_ptr directly; you call .lock() to get a temporary shared_ptr, which is null if the object's already gone.
When a raw pointer is still fine
Raw pointers aren't banned, they're just no longer the default for ownership. A raw T* or a reference is exactly right when you're observing an object you don't own and whose lifetime you know outlives your use of it: a function parameter that just reads or modifies something the caller owns, for instance. The rule: raw pointers for non-owning access, smart pointers for ownership.
Try it yourself: fix a use-after-move bug›
The function below moves data out of a Config object, then tries to read from it afterward. Run it, watch what happens (it may print an empty string, or it may not crash at all, which is exactly the danger of "valid but unspecified"), then fix it by not reading from the moved-from object.