Casting to Rvalue References
Cast lvalues to rvalues to enable move semantics explicitly.
What Is std::move?
std::move is a cast. That is the entire feature. Given an expression, it hands back the same object relabelled as an r-value, so that overload resolution reaches a move constructor or move assignment operator instead of a copy. It is declared in <utility>, it is a constexpr function template, and it does no work of its own: the only thing it produces is a differently typed expression for the compiler to resolve against.
The name is the single most misleading thing about it. Nothing has been moved when std::move returns. Something is moved only when the move constructor or move assignment operator that it made reachable actually runs.
Move semantics are selected by value category, which is a property of an expression, not of an object. An object does not become "moved" or "movable". An expression naming it is either an l-value, which the compiler must assume you will read again, or an r-value, which it is free to strip.
The Cast Is the Whole Mechanism
The cheapest way to see what std::move does is to remove the moving entirely. The two store functions below differ only in the reference type they take, and neither of them touches its argument.
#include <iostream>
#include <string>
#include <utility>
void store(const std::string& caption)
{
std::cout << "l-value overload: " << caption << '\n';
}
void store(std::string&& caption)
{
std::cout << "r-value overload: " << caption << '\n';
}
void relay(std::string&& caption)
{
store(caption); // caption has a name, so the expression is an l-value
store(std::move(caption)); // relabelled, so the r-value overload wins
}
int main()
{
std::string entry{ "estuary at first light" };
store(entry);
store(std::move(entry));
store(static_cast<std::string&&>(entry)); // exactly what std::move writes
relay("gulls over the mudflats");
std::cout << "entry still holds: " << entry << '\n';
return 0;
}
Output:
l-value overload: estuary at first light
r-value overload: estuary at first light
r-value overload: estuary at first light
l-value overload: gulls over the mudflats
r-value overload: gulls over the mudflats
entry still holds: estuary at first light
Three things are worth pulling out of that run.
The second and third calls prove the equivalence. std::move(entry) and static_cast<std::string&&>(entry) select the same overload because they are the same cast, and the library version simply spares you from spelling out the type.
entry survives all three calls with its value intact. Neither overload constructs or assigns anything, so no move ever happened, despite std::move appearing twice. That is the whole point: the cast changes which function gets called and nothing else.
The relay function is the trap that catches almost everybody. Its parameter has type std::string&&, but caption is a name, and any expression that is just a name is an l-value. So the first call inside relay reaches the l-value overload. Once you have accepted an r-value reference, you have to cast again to pass the r-valueness along.
A variable of r-value reference type is an l-value. Inside a move constructor, a move assignment operator, or any function taking
T&&, every use of the parameter needs its own std::move if you want the transfer to continue.
Three Copies or Three Moves
The classic place where you have l-values but want move semantics is a hand-written swap. The values live in named variables, so the compiler's safe default is to copy, and the copies are pure waste: the source of each copy is overwritten a moment later.
FieldNote below announces which special member function runs, so the difference is visible rather than theoretical.
#include <iostream>
#include <string>
#include <string_view>
#include <utility>
class FieldNote
{
public:
explicit FieldNote(std::string_view locality)
: m_locality{ locality }
{
}
FieldNote(const FieldNote& source)
: m_locality{ source.m_locality }
{
std::cout << " copy constructed " << m_locality << '\n';
}
FieldNote& operator=(const FieldNote& source)
{
if (this == &source)
return *this;
m_locality = source.m_locality;
std::cout << " copy assigned " << m_locality << '\n';
return *this;
}
FieldNote(FieldNote&& source) noexcept
: m_locality{ std::move(source.m_locality) }
{
std::cout << " move constructed " << m_locality << '\n';
}
FieldNote& operator=(FieldNote&& source) noexcept
{
if (this == &source)
return *this;
m_locality = std::move(source.m_locality);
std::cout << " move assigned " << m_locality << '\n';
return *this;
}
const std::string& locality() const { return m_locality; }
private:
std::string m_locality{};
};
template <typename T>
void swapByCopy(T& left, T& right)
{
T staged{ left };
left = right;
right = staged;
}
template <typename T>
void swapByMove(T& left, T& right)
{
T staged{ std::move(left) };
left = std::move(right);
right = std::move(staged);
}
int main()
{
FieldNote north{ "Blakeney Point" };
FieldNote south{ "Chesil Bank" };
std::cout << "swapByCopy:\n";
swapByCopy(north, south);
std::cout << "swapByMove:\n";
swapByMove(north, south);
std::cout << north.locality() << " and " << south.locality() << '\n';
return 0;
}
Output:
swapByCopy:
copy constructed Blakeney Point
copy assigned Chesil Bank
copy assigned Blakeney Point
swapByMove:
move constructed Chesil Bank
move assigned Blakeney Point
move assigned Chesil Bank
Blakeney Point and Chesil Bank
Both templates are three lines and both leave the two objects swapped. The first spends one copy construction and two copy assignments, each of which duplicates whatever the class owns. The second spends one move construction and two move assignments, each of which relinks a pointer. For a class holding a string of a few characters the difference is small. For one holding a megabyte buffer it is the difference between three megabytes of allocation and three pointer stores, and the source code differs only by the three casts.
Notice that swapByMove reads left after moving out of it, on the very next line. That is fine, because it does not read the value: it assigns a new one. Moving out of an object and then giving it a fresh value is the normal way to reuse one.
This is why sorting algorithms benefit from move semantics for free. Selection sort and bubble sort are built out of swaps, so an implementation that swaps with moves does one pointer shuffle per exchange rather than one full duplication.
Filling a Container
The second everyday use is handing an object you have finished with to a container. std::vector::push_back has two overloads, and which one you get is decided by the same rule as before.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main()
{
std::vector<std::string> catalog{};
std::string entry{ "estuary at first light" };
catalog.push_back(entry); // l-value: push_back copies
std::cout << "after the copy: [" << entry << "]\n";
catalog.push_back(std::move(entry)); // r-value: push_back may move
std::cout << "after the move: [" << entry << "]\n";
entry = "sanderlings on the tideline"; // assignment is always safe
std::cout << "after reassignment: [" << entry << "]\n";
std::cout << "catalog: " << catalog[0] << " / " << catalog[1] << '\n';
return 0;
}
Output:
after the copy: [estuary at first light]
after the move: []
after reassignment: [sanderlings on the tideline]
catalog: estuary at first light / estuary at first light
The first push_back duplicates the string, which is why entry is unchanged afterwards. The second reaches the overload taking std::string&&, so the new element is move constructed and takes over the existing buffer instead of allocating a second copy of it.
Then look at the second line of output, and do not learn the wrong lesson from it. The empty brackets are what the library behind this site's code runner happened to leave behind. They are not a promise, and the next section explains what actually is.
What a Moved-From Object Is Worth
After you move out of an object, the object is still there. Its destructor will still run. What it contains is a separate question, and the answer depends on who wrote the move operations.
For your own types, you decide, and you should write it down. FieldNote above moves out of its std::string member and says nothing more, so its moved-from state is whatever std::string leaves. A class holding a raw owning pointer has to null the source pointer or the two objects will both delete it. Whatever you choose becomes part of the class's documented interface.
For standard library types, the guarantee is deliberately weak. Unless a particular type promises more, a moved-from library object is left valid but unspecified. Valid means every class invariant still holds, so the object is safe to destroy, safe to assign to, and safe to ask questions that have no preconditions. Unspecified means the value you would read is not defined by the standard, and an implementation is free to change it between releases.
A few types do promise more, and those promises are worth knowing. A moved-from std::unique_ptr or std::shared_ptr is guaranteed to be empty, which is what makes ownership transfer between smart pointers reliable rather than merely conventional.
| Operation on a moved-from object | Safe? |
|---|---|
| Letting it go out of scope, or destroying it | Yes, always |
Assigning a new value with operator= |
Yes, and this is how you reuse it |
clear(), reset(), or any other function with no preconditions |
Yes |
size(), empty(), and other state queries |
Safe to call, but the answer is unspecified |
operator[], front(), back(), pop_back() |
No, their preconditions may not hold |
| Reading its value and relying on what you get | No, this is the mistake the rule exists to prevent |
The last two rows are the ones that bite. operator[] and front() require the container to actually have elements, and nothing guarantees a moved-from container has any. A moved-from std::string may be empty, may still hold its original characters, or may hold something else entirely, and all three are conforming.
Never branch on the value of a moved-from object, and never assume it was cleared. Code that reads correctly on your compiler because a moved-from
std::string came back empty is code that breaks on a different standard library.
When std::move Does Nothing for You
The cast is not free of traps, and none of them produce a diagnostic. The most expensive one is const.
#include <iostream>
#include <string>
#include <string_view>
#include <utility>
class Tag
{
public:
explicit Tag(std::string_view code)
: m_code{ code }
{
}
Tag(const Tag& source)
: m_code{ source.m_code }
{
std::cout << "copied " << m_code << '\n';
}
Tag(Tag&& source) noexcept
: m_code{ std::move(source.m_code) }
{
std::cout << "moved " << m_code << '\n';
}
private:
std::string m_code{};
};
int main()
{
Tag pending{ "NF-118" };
const Tag sealed{ "NF-092" };
Tag transferred{ std::move(pending) };
Tag duplicated{ std::move(sealed) }; // const object: the move constructor cannot bind
return 0;
}
Output:
moved NF-118
copied NF-092
Applying std::move to a const Tag produces a const Tag&&. The move constructor takes a non-const Tag&&, so it is not a candidate, and overload resolution falls back to the copy constructor, which happily binds a const Tag& to anything. The program compiles, runs, and silently does the expensive thing. Marking an object const and then trying to move out of it are contradictory intentions, and const wins.
Three more situations where reaching for the cast is a mistake:
- On a type with nothing to steal. Moving an
int, adouble, or a struct of them copies the bytes either way. The cast costs nothing but it buys nothing either. - On an object you still need. The cast is a promise to the compiler that you are finished with the value. If you are not, do not make it.
- On the operand of a
returnstatement. Writingreturn std::move(local);blocks the copy elision the compiler would otherwise apply, turning a free return into a move. Return the local by name.
Looking Forward
The next lessons build on exactly this cast. std::unique_ptr deletes its copy operations outright, so std::move is not an optimization there, it is the only way to get a resource from one owner to another, and the compiler will reject any attempt to do it by copying. That is move semantics at its clearest: an ownership transfer that the type system can check.
Summary
std::move is a cast, declared in <utility>, that relabels its argument as an r-value so that overload resolution can select a move constructor or move assignment operator. It performs no transfer itself and generates no code.
Value category decides everything. An l-value expression selects copy overloads because the compiler must assume the object will be read again. An r-value expression selects move overloads. std::move changes the expression, not the object.
A named r-value reference is an l-value. Passing a T&& parameter onwards requires a second std::move, or the copy path is taken.
Swapping is the model case. Three copies become three moves, the algorithm is unchanged, and every swap-based sorting algorithm benefits automatically.
Containers take r-values. push_back and its relatives have overloads that move construct the new element rather than copying it.
Moved-from library objects are valid but unspecified. They may be destroyed, assigned to, cleared, or queried, but their value must not be relied upon. std::unique_ptr and std::shared_ptr are the notable types that promise more, being guaranteed empty.
const defeats it silently. std::move on a const object yields a const T&&, which no move operation accepts, so the copy runs instead with no warning.
Apply
std::move only to a non-const object whose value you no longer need, and treat the object afterwards as holding nothing you may read. Assigning it a new value is the correct way to bring it back into use.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Casting to Rvalue References - Quiz
Test your understanding of the lesson.
Practice Exercises
Custom Swap with std::move
Implement an efficient swap function using std::move that works with a resource-managing class. Demonstrate the performance difference between copy-based and move-based swapping.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!