C++ / MOVE SEMANTICS AND SPECIAL MEMBER FUNCTIONS
std::move and the cast that enables moving
Read std::move as a compile-time cast to an rvalue reference, and predict when it triggers a real move and when it silently copies.
What you will learn
- Rewrite std::move(x) in your head as static_cast<T&&>(x): no runtime work happens.
- Spot the silent copy when std::move is applied to a const object or a trivial type.
- Re-apply std::move when passing a named rvalue reference parameter onward.
- Judge whether a move happened by the overload chosen, not by the presence of the cast.
Understanding std::move and the cast that enables moving
std::move performs no move. It is a one-line function template whose body is return static_cast<std::remove_reference_t<T>&&>(t);, so after inlining it emits zero instructions and leaves the argument's bytes untouched. What it changes is the value category of the expression it wraps: x is an lvalue, std::move(x) is an xvalue naming the same object. That difference is visible to the compiler at exactly one moment, when it picks an overload.
Treat std::move as a permission slip handed to whatever receives the expression. The stealing, if any, is done by the move constructor, the move assignment operator, or a T&& parameter that decides to take the guts. If no overload can bind an rvalue better than a const T& overload can, resolution quietly falls back to the copy with no diagnostic; that is what happens for std::move on a const object, and for int, where there is nothing to steal.
The reason you need std::move so often is that names are lvalues, including names whose declared type is an rvalue reference. Inside void sink(Widget&& w), the expression w is an lvalue, so Widget copy = w; copies, and only std::move(w) casts it back. The same rule explains why move constructor bodies are full of std::move(other.member): other has a name, so its members are lvalues, and without the cast every member would be copied.
<iostream>
<string>
<utility>
void take(std::string& s) { std::cout << "lvalue overload: " << s << '\n'; }
void take(std::string&& s) { std::cout << "rvalue overload: " << s << '\n'; }
int main() {
std::string a = "hello";
take(a); // a is an lvalue
take(std::move(a)); // same object, the expression is now an xvalue
std::cout << "a after the cast: " << a << " (size " << a.size() << ")\n";
std::string&& r = std::move(a); // r is a name...
take(r); // ...so this expression is an lvalue again
take(std::move(r));
}
std::move is a cast that changes an expression's value category; the move itself, if it happens at all, is performed by the overload that cast selects.
Worked examples
const kills the move without a word
The same std::move call selects the move constructor from a mutable lvalue and the copy constructor from a const reference.
<iostream>
<utility>
struct Loud {
Loud() = default;
Loud(const Loud&) { std::cout << "copy\n"; }
Loud(Loud&&) noexcept { std::cout << "move\n"; }
};
void keep_by_value(Loud x) { (void)x; }
void keep_from_const(const Loud& in) {
Loud x = std::move(in);
(void)x;
}
int main() {
Loud a;
std::cout << "from a non-const lvalue:\n";
keep_by_value(std::move(a));
Loud b;
std::cout << "from a const reference:\n";
keep_from_const(b);
}
Example explained
Line 1keep_by_value(std::move(a)) initialises the by-value parameter from an xvalue of type Loud, so Loud(Loud&&) wins.
Line 2In keep_from_const, in has type const Loud&, so std::move(in) is a const Loud&& expression.
Line 3Loud(Loud&&) cannot bind a const rvalue, but Loud(const Loud&) can, so the copy constructor is selected.
Line 4Nothing about this is an error, which is why the copy is easy to miss: the cast succeeded, the optimisation did not.
A named rvalue reference is still an lvalue
Inside a function taking Loud&&, the parameter must be cast again or it copies.
<iostream>
<utility>
struct Loud {
Loud() = default;
Loud(const Loud&) { std::cout << "copy\n"; }
Loud(Loud&&) noexcept { std::cout << "move\n"; }
};
void sink(Loud&& r) {
Loud first = r; // r has a name, so this is an lvalue
Loud second = std::move(r); // cast back to an rvalue
(void)first;
(void)second;
}
int main() {
sink(Loud{});
}
Example explained
Line 1sink(Loud{}) binds r to a temporary; binding runs no constructor, so nothing prints yet.
Line 2Loud first = r; uses the name r, and every use of a name is an lvalue expression, so the copy constructor is chosen.
Line 3std::move(r) restores the rvalue category of the very same object, so the second initialisation moves.
Line 4This is why a move constructor that forwards members must write std::move(other.member) rather than other.member.
The cast is only a type, not an action
std::move(s) and static_cast<std::string&&>(s) are the same expression, and moving an int copies it.
<iostream>
<string>
<type_traits>
<utility>
int main() {
std::string s = "abc";
std::cout << std::boolalpha
<< std::is_same_v<decltype(std::move(s)), std::string&&> << '\n'
<< std::is_same_v<decltype(static_cast<std::string&&>(s)), std::string&&> << '\n';
int n = 41;
int m = std::move(n);
std::cout << n << ' ' << m << '\n';
}
Example explained
Line 1decltype of an xvalue expression yields T&&, so both traits report std::string&& and the two spellings are equivalent.
Line 2The operands of decltype are unevaluated, so std::move is not even called there; the whole effect is in the type system.
Line 3int has no move constructor to select, so int m = std::move(n); is an ordinary copy.
Line 4n still prints 41, which shows the cast never had any power to modify its operand.
Important notes
std::move is declared in <utility>. Code frequently compiles without that include because another header pulled it in, and then breaks after an unrelated library or compiler update.
The return type is std::remove_reference_t<T>&& rather than T&& because T deduces to X& for lvalue arguments; without stripping the reference, reference collapsing would turn T&& back into X& and the cast would do nothing.
Common mistakes
Writing std::move(v); as a standalone statement and expecting v to be emptied: the resulting xvalue is discarded, no constructor or assignment runs, and the object is unchanged.
Calling std::move on a const T& parameter: the result is a const T&& that only the copy constructor can bind, so a full copy is paid for silently with no warning.
Returning std::move(local): the cast stops the compiler from constructing the local directly in the caller's storage, so you force a move where plain return local; would have cost nothing.
Try it yourself
Change, predict, then run
Write a struct whose copy and move constructors each print a distinct line, then construct three objects from x, from std::move(x), and from std::move(cx) where cx is a const reference bound to x. Write down the three lines you expect before you compile, then compare.
Open the C++ workspaceCheck your understanding
A function is declared void store(std::string&& s) and its body does member = s;. The caller writes store(std::move(text));. Why is text's buffer still copied?
- Because s is a name, so the expression s is an lvalue and copy assignment is selected
- Because std::move only affects temporaries, not named variables like text
- Because std::move(text) already moved out of text, so s holds a fresh copy
- Because std::string's move assignment operator only accepts a const std::string&&
Show answer
Inside store, s is declared as an rvalue reference, but every use of the name s is an lvalue expression, so member = s picks copy assignment; member = std::move(s) fixes it. Option 3 is tempting because the caller did write std::move, but that cast moved nothing: it only made the argument bind to the rvalue reference parameter, and no constructor or assignment ran at that point.