Move semantics are overload selection and type-defined state transfer, not an automatic transfer of ownership. Begin with an expression’s value category, then choose a parameter type that describes ownership and lifetime.
Expression Categories #
Every non-void expression is exactly one of these categories:
- An lvalue identifies an object, function, or bit-field.
- An xvalue identifies an object whose resources may be reused.
- A prvalue computes a value or initializes an object.
Lvalues and xvalues are glvalues; prvalues and xvalues are rvalues. A named variable is an lvalue expression even when it has type T&&.
int& counter();
int&& cache_slot();
counter() = 42; // The call is an lvalue.
int&& slot = cache_slot();
consume(slot); // `slot` is an lvalue expression.
consume(std::move(slot)); // Casts it to an xvalue.
A call returning T is a prvalue, one returning T& is an lvalue, and one returning T&& is an xvalue. std::move performs an xvalue cast; it neither moves data nor ends the source object’s lifetime.
Move Construction Is Overload Selection #
Reference binding provides the relevant distinction:
T&binds compatible lvalues.const T&binds lvalues, xvalues, and prvalues.T&&binds xvalues and prvalues.
std::string source = "payload";
std::string destination = std::move(source); // Selects a move constructor when viable.
The selected constructor defines the transfer. It might transfer an allocation, copy a small representation, or have an effect indistinguishable from copying. Returning a local by value should normally use return local;: copy elision may construct the result directly, and when elision does not apply an eligible local can be treated as an xvalue for overload selection. return std::move(local); can inhibit named return value optimization.
Forwarding References Preserve the Caller’s Category #
A T&& in a deduced template parameter is a forwarding reference. For an lvalue argument, T deduces as U&; for an rvalue, it deduces as U. Reference collapsing reduces U& && to U&, so std::forward<T> can reproduce the original category.
template<class T>
void relay(T&& value) {
consume(std::forward<T>(value));
}std::move(value) would turn both cases into xvalues and can unexpectedly move from an lvalue caller. A T&& that does not use deduction—for example, a member of a class template whose T is already fixed—is an ordinary rvalue reference, not a forwarding reference.
Parameter Design Starts with Ownership #
Use the parameter form that describes the callee’s contract:
| Form | Contract |
|---|---|
const T& | Borrow for read-only use during the call. |
T& | Borrow and mutate during the call. |
T | Take an independent value; callers copy or move into it. |
T&& | Require an rvalue that the callee may consume; ownership is not implied. |
std::unique_ptr<T> | Transfer exclusive ownership. |
std::shared_ptr<T> | Share ownership when the callee retains a share. |
T* | Borrow, with nullability made explicit by the API. |
A constructor that stores an independent value can take it by value and move from its parameter:
class Message {
public:
explicit Message(std::string text) : text_{std::move(text)} {}
private:
std::string text_;
};An lvalue caller performs one copy into the parameter; an rvalue caller can move into it. Passing by value is inappropriate for a query or for an object that the callee merely borrows.
Do not use FILE*&& or another raw-pointer rvalue reference to imply ownership. Moving a raw pointer copies its address; both names still point at the same resource. Use an ownership-bearing wrapper, such as std::unique_ptr<T, Deleter>, when the call transfers destruction responsibility. A function that only uses the object owned by a smart pointer should take T&, const T&, T*, or const T* according to access and nullability; it need not accept the smart-pointer handle.
Moved-from Objects Remain Alive #
After a standard-library object is moved from, it has a valid but unspecified value unless a specification says more. Destruction, assignment, and operations whose preconditions accept that state are valid. Do not assume a moved-from std::vector is empty or retains capacity.
std::vector<int> buffer = read_row();
rows.push_back(std::move(buffer));
buffer.clear(); // Valid, but capacity reuse is not guaranteed.
A borrow must not outlive its owner. Moving an owning unique_ptr destroys neither the pointee nor all pre-existing borrows, but later destroying or resetting the new owner can make those borrows dangle. Move operations solve representation transfer; they do not remove the need to state lifetimes.
Comments
Read or comment on GitHub (sign-in required to comment)