Articles
C++ Move Semantics Interview Questions: Value Categories, Ownership, and Copy Elision
Prepare for C++ move semantics interviews with value-category traces, const pitfalls, ownership, noexcept, forwarding, moved-from state, and copy elision.
C++ move semantics interview questions are easiest to answer when you separate an expression's value category from the operation eventually selected. std::move does not move a resource by itself. It produces an expression that can participate in move-aware overload resolution; the receiving constructor or assignment operator determines what actually happens.
This guide uses C++17 as the baseline and labels the distinction between guaranteed prvalue construction and optional named return value optimization. The examples are original traces. Standard-library contracts and the C++ Core Guidelines support the rules, but a custom class must define its own valid ownership behavior.
Here, we focus on the cases that make otherwise confident answers fail: named rvalue references, const objects, moved-from state, and unnecessary moves in return statements.
What are lvalues, prvalues, and xvalues?
Value categories classify expressions, not simply variable types. An lvalue identifies an object in a way commonly associated with a persistent named expression. A prvalue computes a value or initializes an object under the language's rules. An xvalue identifies an object whose resources may be reused in suitable contexts.
You do not need a philosophical definition to start a useful trace. For std::string s = "hello";, the expression s is an lvalue. The expression std::move(s) is an xvalue. A fresh std::string("hello") is a prvalue.
Crucially, a variable declared with type T&& is still an lvalue when used by its name. Its type tells you what it can bind to; its named expression category tells you which overload may be selected at the next use.
Avoid the shortcut "rvalues live on the stack temporarily and lvalues do not." Storage duration, lifetime, type, and value category are different concepts. A good answer uses a concrete expression and predicts the selected operation rather than tying the category to a memory region.
Does std::move actually transfer ownership?
No. Consider an original trace with a standard string:
std::string source = "report";
std::move(source); // result discarded
std::string copy = source; // copy construction
std::string moved = std::move(source); // move construction
The discarded cast does not transfer ownership. The final initialization presents an xvalue to the constructor, making the move constructor applicable. It is that constructor, not the cast, that performs the operation.
Even then, "move" does not universally mean transferring one pointer. An integer move behaves like copying its value. A small-string implementation may store characters inside the object, so moving need not resemble a heap-buffer handoff. Describe the contract before guessing the representation.
For a custom resource-owning type, a move constructor typically transfers ownership and leaves the source safe to destroy or assign to according to its documented invariant. If it merely copies a raw owning pointer without neutralizing the old owner, both objects may later release the same resource.
That bug is not fixed by writing std::move at the call site. The receiving type must implement ownership correctly, which is one reason to prefer well-designed RAII members and the rule of zero.
Why does moving a const object often copy?
std::move preserves const qualification. Moving a const T produces a const T&& expression. A typical move constructor takes T&&, because transferring resources usually requires modifying the source. It cannot bind that non-const reference to a const object.
const std::string original = "report";
std::string result = std::move(original);
For std::string, this uses copying rather than its ordinary move constructor. The const-reference copy constructor remains a viable match. Do not claim that the presence of std::move proves a performance optimization occurred.
The general answer depends on available overloads. A custom type could define unusual const-rvalue operations, and a move-only type with no viable copy may make a similar expression ill-formed. Name the type and inspect its constructors.
This produces a practical design question: should a local object be const if ownership will be transferred later? Use const to express real immutability, not as a universal decoration that accidentally blocks the intended operation. The right choice follows the object's lifecycle.
Can you predict these overloads?
Assume a teaching type Item has a copy constructor from const Item& and a move constructor from Item&&. No overload has special side effects beyond logging which one was selected.
| Expression used to initialize a new Item | Usual selected constructor | Reason |
|---|---|---|
| Named non-const item | Copy | Named expression is an lvalue |
| std::move(non-const item) | Move | Non-const xvalue matches Item&& |
| std::move(const item) | Copy | Ordinary move cannot bind const source |
| Named Item&& parameter | Copy | Parameter name is an lvalue expression |
| std::move(named Item&& parameter) | Move | Explicitly presents it as an xvalue |
Do not include temporary construction in this table without discussing elision. Under C++17, initializing from a same-type prvalue can construct the destination directly, so there may be no intermediate object to copy or move.
A useful interview technique is to annotate each expression in three steps: static type including const, value category, and viable overloads. This method scales better than memorizing individual snippets.
If you instrument constructors, compare observations with the standard guarantees and compiler settings. A particular optional elision decision is not a universal language rule. Logging can help explain a case, but it does not replace the contract.
What can I do with a moved-from object?
For standard-library types, unless otherwise specified, moved-from objects remain valid but have an unspecified state. Valid means the type's invariants hold; it does not mean the previous value is preserved or that the object is always empty.
You can destroy such an object and perform operations whose preconditions are satisfied. Reassigning a string is a straightforward example. Calling an operation that requires nonemptiness without checking the state is not justified merely because the object is valid.
Some types provide stronger guarantees. A moved-from std::unique_ptr is empty under its move contract. Do not generalize that guarantee to every container, string, or user-defined class.
For an original ownership exercise, move a unique_ptr holding one resource into a new owner, then check the source before dereferencing it. Explain why checking is essential and why copying was prohibited in the first place. The point is a single owner, not a faster spelling for assignment.
In code review, repeated use after a move deserves attention even when technically valid. Make the next step explicit: reinitialize, test the relevant state, or end the old owner's role. This keeps the program understandable as well as correct.
How do move construction and move assignment differ?
Move construction initializes a new object. Move assignment replaces the state of an existing object, which may already own a resource. Assignment must therefore handle the destination's old ownership as well as the incoming source.
Imagine two file-handle wrappers, A and B, each owning a different handle. Constructing C from A starts with no old C handle to release. Assigning B from A must release or otherwise resolve B's previous handle before establishing the new ownership, according to the wrapper's contract.
Self-move is another review case. A robust move-assignment design should leave a valid result under its stated requirements, but do not casually promise that a self-moved object retains its old value. Prefer standard RAII members whose behavior is already specified.
Also remember that declaring special member functions affects which others are implicitly generated. A hand-written destructor can change the availability of implicit moves. Before adding a custom move constructor, ask whether replacing raw ownership with unique_ptr, vector, or another RAII member would remove the need for custom special members entirely.
Why does noexcept matter for moves?
Containers sometimes relocate elements while preserving exception-safety guarantees. A move that can throw may make rollback difficult after some source elements have already been modified. A nonthrowing move can therefore change which relocation strategy a container can safely choose.
Do not reduce this to "vector always copies unless move is noexcept." The actual behavior depends on the operation, the type's copy availability, and its exception guarantees. Move-only types and throwing moves require particular care.
For a custom type, declare a move noexcept only when its implementation and members justify that promise. If an exception escapes a noexcept function, the program terminates; the annotation is not a hint that makes throwing harmless.
An interview follow-up might ask whether a logging allocation inside an otherwise simple move is acceptable. That allocation can fail and complicate the guarantee. Keep move operations small and consistent with the resource invariant, and avoid adding work that undermines the promised exception behavior.
What is copy elision, and why can return std::move be worse?
Since C++17, certain same-type prvalue initializations construct the destination directly. This is different from optional named return value optimization, commonly called NRVO, where the compiler may construct an eligible named local directly in the caller's result storage.
Item make_direct() {
return Item{};
}
Item make_named() {
Item value;
return value;
}
The first illustrates direct prvalue construction under the relevant C++17 rules. The second is an NRVO candidate. If optional NRVO is not performed, the return rules can still enable moving in eligible cases; exact details depend on the language version and type.
Writing return std::move(value); can prevent NRVO because the returned expression is no longer the eligible bare local name. It may therefore introduce a move where direct construction was possible. Do not add it as a reflex.
Return the local naturally unless a specific, understood case requires otherwise. A strong answer distinguishes "the compiler often optimizes this" from "the language guarantees this form of construction."
When should I use std::forward instead?
Use std::forward when a forwarding-reference parameter should preserve the caller's value category through a wrapper. Use std::move when you deliberately allow a known object's resources to be consumed. They express different ownership intentions.
template<class T>
void relay(T&& value) {
consume(std::forward<T>(value));
}
Here T is deduced in the forwarding-reference pattern. The named value expression is still an lvalue inside the function, so forwarding uses the deduced type to recover the intended category. Not every T&& in every context is a forwarding reference.
Do not forward the same potentially consumable argument repeatedly without understanding the consequences. The first call may move from it. A wrapper that logs and then consumes needs to ensure logging does not accidentally consume the object too.
Explain the function's contract in ordinary words: "This wrapper preserves whether the caller supplied an lvalue or an rvalue." That sentence is clearer than saying forwarding is just a more advanced move.
How should I practice ownership traces?
Write a small move-aware type around an existing RAII member and log construction, assignment, and destruction in a controlled test. Predict which operations are required, optional, or absent before compiling. Include a const source, a named rvalue-reference parameter, and both return forms above.
Then remove the logging and explain the resource invariant. Who owns the resource before the operation? Who owns it afterward? What state may the old owner have? Which operation can throw, and what remains valid if it does?
The strongest answer is not "moves are faster than copies." It is a trace that separates expression category, selected operation, ownership, and lifetime without guessing any of them.
Sources and Further Reading
Checked September 23, 2026. Examples use a C++17 baseline; the living draft can include later changes, so language-version distinctions are stated explicitly.