Skip to content
VLSI Mentor

C++ Part I · Module 13 · Copy and Move Semantics

std::move and the Moved-From State

Every measurement in this module that produced a move rather than a copy did so because something wrote std::move, and the function has the most misleading name in the standard library. It does not move anything. Chapter 13.6 established that value category is a property of an expression rather than of an object, and that an expression can therefore refer to an object while making a different claim about whether anybody still owns it — which is exactly what this function provides and the whole of what it provides. The measurement is direct: calling it on a fifty-nine character string and discarding the result leaves that string with fifty-nine characters in the same buffer at the same address. Nothing happened. What makes a move happen is the constructor or assignment operator that the resulting expression then selects, and that function is where the resource actually changes hands. The second half of the chapter is the question the previous two deferred: once something has moved from an object, that object is still alive, still has a destructor that will run, and is still a variable you could use by accident or on purpose — so what, precisely, remains true of it?

Advanced17 min readC++Part IFundamentalsMove Semantics

Part I · Module 13 · Chapter 13.9 · std::move and the Moved-From State

1. What It Is For

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::string t = std::move(s);        // move-construct t from s

Chapter 13.7 §3 measured that T&& refuses to bind to a named object, and Chapter 13.7 §6 measured the consequence: inside a function, a named parameter is an lvalue, so handing it on selects the copying overload.

std::move is how you override that. It is the caller saying: I am finished with this; treat it as expiring.

std::move grants permission. The move itself is performed by whichever function the resulting expression selects.

2. It Moves Nothing — Measured

Azvya Education Pvt. Ltd.VLSI Mentor
stdmove.cpp — the relevant part
    std::string s = "a long string well past the small-string optimisation limit";
    const void* before = s.data();

    (void)std::move(s);                       // call it, discard the result

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. std::move(s) alone, result discarded:
   s.size()=59  data==before? YES -- unchanged

Fifty-nine characters, same buffer, same address. The call did nothing observable at all.

Now the same expression used to initialize something:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2. now actually move-construct from it:
   t.size()=59  s.size()=0
   t.data()==before? YES -- buffer transferred

t holds the original buffer — the same address before recorded — and s is now empty. The transfer happened in std::string's move constructor, which was selected because std::move(s) is an rvalue expression.

So the division of labour is:

What it does
std::move(s)produces an expression of rvalue category referring to s
the move constructoractually takes the buffer and clears the source

3. What It Returns

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
is_lvalue_reference<decltype(std::move(t))> = 0
is_rvalue_reference<decltype(std::move(t))> = 1

An rvalue reference. That is the whole implementation — it is a cast, written roughly as:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    template <typename T>
    constexpr std::remove_reference_t<T>&& move(T&& t) noexcept
    { return static_cast<std::remove_reference_t<T>&&>(t); }

A static_cast to T&&, and nothing else. No branching, no copying, no bookkeeping. It compiles to no instructions whatsoever — the cast changes how the expression is classified, which is a compile-time property.

A better name would have been std::as_expiring or similar. The name describes the intent of the caller rather than the action of the function, which is why it confuses everyone once.

4. Where to Write It

Three situations, and only three:

Passing a named object you are finished with:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    store(std::move(localBuffer));       // I will not use localBuffer again

Passing on a T&& parameter — Chapter 13.7 §6's hazard:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void sink(Buffer&& b) { store(std::move(b)); }    // without move: a copy

And moving a member out in an accessor or a transfer function, which is deliberate and rare.

Where not to write it is §9.

(A related function, std::forward, exists for generic code that must preserve whichever category it received rather than always claiming rvalue. It belongs with Module 18's templates and is named here only so the pair is recognisable.)

5. The Moved-From State

Now the question Chapters 13.7 and 13.8 both deferred. After something has moved from an object, that object:

Still exists. It is a variable in scope with a lifetime that has not ended.

Will still be destroyed. Its destructor runs normally at the end of its scope — Chapter 12.7 §6's table applies unchanged.

And holds whatever the move operation left it holding. Chapter 13.8 §3 established that leaving it destructible is the move's obligation.

Measured on a std::string:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after being moved from:
  s.size()     = 0
  s.empty()    = 1
  s.capacity() = 22
  after s = "reassigned": "reassigned" size=10

Three observations:

It is observable. size() and empty() can be called and give sensible answers.

It is empty, and capacity() is back to 22 — the inline small-string capacity Chapter 9.5 measured, because the heap buffer was handed away and the string reverted to its internal storage.

And it is assignable. s = "reassigned" worked, giving a string of size 10. The object is fully functional again.

Chapter 13.8 §5 measured the equivalent for a user-defined type: size=0 data=0x0.

6. The Rule

A moved-from object is valid but unspecified.

Those two words carry the whole guarantee, and they are worth separating:

Valid means: it satisfies its invariant, its destructor is safe to run, and any operation with no preconditions may be called on it. Assignment, clear(), size(), empty(), destruction — all fine.

Unspecified means: you may not assume which valid value it holds. For std::string the measurement showed empty, and that is what every implementation does, and the standard does not promise it. So reading the value and relying on it is a bug even though it will appear to work.

The practical boundary:

On a moved-from objectAllowed?
destroy ityes
assign a new value to ityes
call clear(), reset(), or similaryes
ask size() / empty()yes, but do not rely on the answer
read its value and use itno — unspecified
call an operation with a precondition, e.g. front() on an empty containerno

The one-line version: assign to it or destroy it; do not read it.

7. Two More Moved-From States

Section 5 measured a std::string. Two other standard types are worth measuring because they behave differently, and because one of them is the case you will meet most often.

A std::vector:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
vector moved-from : size=0 capacity=0 empty=1  (was capacity 5)
                    after push_back(9): size=1 front=9

Both size() and capacity() are 0 — the whole buffer was handed over, and unlike std::string there is no inline storage to fall back to, so capacity went to zero rather than to 22. And push_back worked immediately afterwards, giving a usable one-element vector.

A std::unique_ptr:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unique_ptr moved-from: get()=0x0  bool=0  (q holds 42)

get() returns null and the object converts to false. This one is the important case, and it is the only standard type whose moved-from state is actually guaranteed: a moved-from unique_ptr is null, by specification, because that is the only state in which it can safely be destroyed. Chapter 14.2 is unique_ptr properly.

So the three types measured in this chapter:

TypeAfter being moved fromGuaranteed?
std::stringempty, capacity() back to 22no — valid but unspecified
std::vectorsize() 0, capacity() 0no — valid but unspecified
std::unique_ptrnullyes

The pattern worth noticing: the types whose moved-from state is unspecified are the ones where several valid empty states exist, and the type whose state is guaranteed is the one where only a single state is safe to destroy. That is not a coincidence — Chapter 13.8 §3 established that leaving the source destructible is the move's obligation, and for a unique_ptr "destructible" means exactly "null".

And it gives a practical rule for reading code: a moved-from unique_ptr may be tested (if (p)), because null is promised. A moved-from container may not be relied upon for anything but assignment and destruction.

8. Why "Unspecified" Rather Than "Empty"

A reasonable objection: every implementation leaves a moved-from std::string empty, so why not promise it?

Because the promise would cost something. A move is supposed to be the cheapest possible operation — Chapter 13.8 §1's two assignments — and guaranteeing a particular final state for the source can require extra work for some types. Leaving it unspecified lets each type's move do the minimum.

For your own types you may of course be stricter, and Chapter 13.8's Buffer is: it always leaves nullptr and 0. That is a stronger guarantee than the standard requires and it is free, because the clear was necessary anyway.

(A related standard utility, std::exchange, is the idiomatic way to write "take this value and leave a specified one behind", which makes the clear explicit in one expression. It belongs to Module 21.)

9. Moving from a Member

One pattern worth seeing, because it is where the moved-from state stops being hypothetical:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class Message {
    public:
        std::string takePayload() { return std::move(payload_); }   // the member is now empty
    private:
        std::string payload_;
    };

After takePayload(), the Message object's payload_ is moved-from — valid, unspecified, probably empty. The object is still usable but its invariant has quietly changed: a Message is now a message whose payload may have been taken.

Chapter 11.5 §2 required an invariant to hold for the object's whole lifetime, and this function weakens it. That is sometimes exactly the design — a one-shot transfer — and it should be deliberate and named as such (take, release, extract), never hidden behind an innocuous accessor name.

10. Where Not to Write std::move

The important negative, and it is measured.

Never on a return of a local object:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer make() { Buffer x(8); return std::move(x); }      // slower
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]

Chapter 13.12 measures the cost: plain return x; performed zero operations, and return std::move(x); performed one move. The std::move defeated an elision that was removing the operation entirely, making the code strictly worse — and the warning is in the standing flag set, unlike several others in this module.

Never on something you will use again. The object is then valid-but-unspecified and any read is a bug.

Never on a const object — Chapter 13.7 §3 measured T&& refusing to bind, so the move silently becomes a copy.

And do not scatter it for its own sake. For a type holding only scalars a move is a copy (Chapter 13.8's callout), so std::move on an int accomplishes nothing.

11. What to Actually Do

Read std::move(x) as "treat x as expiring", not as an action.

Write it only where you can name the receiving operation and this is the last use.

Write it when passing on a T&& parameter — otherwise it copies (Chapter 13.7 §6).

Never write it on a return of a local — measured slower, and the compiler warns.

Treat a moved-from object as: assign to it or destroy it, do not read it.

Do not rely on it being empty even though it measurably is.

Leave your own types in a specified state anyway — the clear was necessary regardless.

Name a function that moves out of a member honestly — take, release, extract.

And do not use it on const objects or scalars — it accomplishes nothing.

12. Mistakes Worth Making Once

Expecting std::move(s) alone to do something. Measured: 59 characters, same buffer, unchanged.

return std::move(x); Measured: 1 move where plain return x; cost 0 — and the compiler warns at the standing flags.

Reading a moved-from object's value. Valid but unspecified.

Relying on a moved-from std::string being empty. It measurably is; that is not a guarantee.

Using it on a const object. T&& refuses to bind, so it copies silently.

Using it on an int. A move is a copy for scalars.

Forgetting it when passing on a T&& parameter. The commonest silent copy.

Hiding a member-move behind an accessor name. The object's invariant quietly changed.

Calling a precondition-bearing operation on a moved-from object — front() on an empty container.

13. Interview Relevance

"What does std::move do?" It casts its argument to an rvalue reference — it moves nothing. The measurement that lands: calling it and discarding the result leaves the object entirely unchanged. Candidates who say "it moves the object" have the common misconception.

"So where does the move happen?" In the move constructor or move assignment operator that the resulting rvalue expression selects.

"What can you do with a moved-from object?" It is valid but unspecified — assign to it or destroy it; do not read its value. Naming both words is the complete answer.

"Is a moved-from std::string empty?" In practice always; by guarantee no. This distinction is a good discriminator.

"What is wrong with return std::move(x);?" It prevents copy elision and makes the function slower, and the compiler warns — moving a local object in a return statement prevents copy elision.

"When must you write std::move?" When passing on a named T&& parameter, and when handing over a local you are finished with.

14. What You Should Be Able to Say Now

  1. std::move grants permission; the move is performed by the function it selects.
  2. Measured: it moves nothing — a 59-character string was unchanged, same size, same data pointer.
  3. Measured: it returns an rvalue reference — is_rvalue_reference = 1, is_lvalue_reference = 0.
  4. It is a static_cast and compiles to no instructions.
  5. The name describes the caller's intent, not the function's action.
  6. Measured transfer: after move-construction, t held the original buffer and s.size() was 0.
  7. Write it in three places: handing over a local you are done with, passing on a T&& parameter, and deliberately moving out a member.
  8. A moved-from object is valid but unspecified.
  9. Valid = invariant holds, destructor safe, precondition-free operations fine. Unspecified = do not rely on the value.
  10. Measured: moved-from std::string had size() 0, empty() 1, capacity() 22 (back to the inline capacity), and was assignable.
  11. Assign to it or destroy it; do not read it.
  12. "Unspecified" exists so a move can do the minimum — your own types may be stricter for free.
  13. Measured: return std::move(x); cost 1 move where return x; cost 0, with -Wpessimizing-move in the standing flags.
  14. Never on a const object or a scalar — it does nothing.

15. Exercises

Exercise 1 — predict. For std::string s = "long..."; say what s.size() is after: (a) std::move(s); alone (b) std::string t = std::move(s); (c) s = "new"; following (b).

Exercise 2 — name the receiver. For each, say which function performs the move or state that none does: std::move(x);, f(std::move(x));, T y = std::move(x);, y = std::move(x);

Exercise 3 — allowed or not. On a moved-from std::vector<int> v: v.size(), v.push_back(1), v[0], v.clear(), v = other, v.front().

Exercise 4 — the pessimisation. Explain why return std::move(x); is slower than return x;, and quote the warning.

Exercise 5 — the silent copy. void sink(Buffer&& b) { other(b); } — say what happens and why, and give the fix.

Exercise 6 — const. Explain what std::move(constObject) accomplishes, citing Chapter 13.7's binding table.

Exercise 7 — honest naming. A class has std::string getPayload() { return std::move(payload_); }. Say what is wrong with the name and what the object's invariant now is.

Exercise 8 — valid but unspecified. A colleague's test asserts a moved-from string is empty and it passes. Explain why the test is wrong anyway.

16. Summary

  • std::move grants permission; the receiving function performs the move.
  • Measured: it does nothing — a 59-character string unchanged; it returns an rvalue reference and is a static_cast.
  • The name describes intent, not action.
  • Write it when handing over a local you are finished with, when passing on a T&& parameter, and for a deliberate member transfer.
  • A moved-from object is valid but unspecified — measured as size() 0, capacity() 22, and assignable.
  • Assign to it or destroy it; do not read it.
  • "Unspecified" lets a move do the minimum; your own types can be stricter for free.
  • Measured: return std::move(x); costs a move that return x; does not — and the compiler warns at the standing flags.
  • Useless on const objects and scalars.

Five functions now exist for a resource-owning class and the module has treated each one as a separate decision. They are not separate, and the way they interact is not guessable — Chapter 13.8 §8 already mentioned that a destructor suppresses the generated move operations, which means a class written to Chapter 13.5's Rule of Three loses the ability to move without anything being said. The next chapter measures the interactions, and the measurement needed care: the obvious way to probe them reports that a class with a user destructor is still move-constructible, which is true and means something different from what it appears to mean, because a copy constructor will satisfy a request for construction from an rvalue. Counting which function actually runs gives the opposite answer.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.