---
title: 'Move Semantics'
source: 'https://academia.sh/en/courses/cpp-standard-library/move-semantics'
course: 'Resource Management and the Standard Library'
language: en
updated: '2026-09-26T03:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'distinguishes among rvalue, lvalue, and rvalue reference'
    - 'builds an example showing that a std::move call is nothing but a type conversion'
    - 'correctly handles a moved std::string that stays in a valid but unspecified state'
    - "proves with a printed line that the source's destructor still runs after a move"
---

# Move Semantics

std::move is nothing but a type conversion; the actual move is done by the move constructor and the move assignment operator. Measuring what is left in the source after a std::string is moved.

The previous lesson showed that a moved `unique_ptr`'s source is emptied by a promise the standard gives: once the move finishes, the source becomes `nullptr`, and this guarantee comes from the pointer's own type. But not every type speaks with the same certainty. `unique_ptr` can cheaply guarantee emptiness because it carries nothing but a pointer; when a type that carries its own data, such as `std::string`, is moved, the standard does not give the same promise. This lesson measures that less certain outcome.

## Rvalue, Lvalue, Rvalue Reference

Whether an expression is an lvalue or an rvalue has less to do with whether it has a name and more to do with whether its address can be taken. A named variable like `s` is an lvalue; a temporary result like `std::string("temp")` or the expression `std::move(s)` returns is an rvalue. An rvalue reference, declared with `T&&`, can only bind to rvalues; this binding signals to the compiler, "this value is a temporary nobody needs anymore, you may take its content."

`std::move` looks like a function but actually moves no data at all; the only thing it does is convert an lvalue it holds into an rvalue reference — it is nothing but a plain `static_cast<T&&>` call. This conversion alone calls no move constructor or move assignment; it only lets the compiler's overload resolution pick the rvalue-specific version, if one exists.

That is why the expression `std::move(s)` by itself produces no side effect: `s`'s type does not change, its address does not change, its content does not change. The side effect appears only when this expression is passed as an argument to a constructor or assignment operator, and the compiler picks the move version for that call. `std::move(s)` can also be written without ever entering a function call, for instance just assigned to a variable and never used again — in that case no move happens at all, because no move constructor or move assignment is ever called. The name "move" being in it does not mean the call itself guarantees a move; it only expresses a request, a permission.

When a class declares both a `T(const T&)` and a `T(T&&)` version, the compiler decides which one is called by looking at the value category of the argument: given a named lvalue, the `const T&` version is picked; given an rvalue produced by `std::move` or already temporary, the `T&&` version is picked. This selection requires no extra signal from the caller — the same call syntax (`T fresh(source);`) routes to a different constructor depending on `source`'s type. In a class with no `T&&` parameter, the only option, the `const T&` version, runs in either case; being handed an rvalue makes no difference here, because there is no other overload for the compiler to pick.

## The Move Constructor and the Move Assignment Operator

A class's move constructor is declared with the signature `T(T&& other)`, and its move assignment operator with the signature `T& operator=(T&& other)`. Both are written to hand over the source's resource — a buffer, a handle, a number — to the target; their difference from the copying versions is that they take over what already exists instead of opening a new resource.

The `Box` class below shows this by hand. The move constructor moves `other`'s `label_` member into its own `label_` member — here again `std::move` only converts `other.label_` into an rvalue; the actual move is done by `std::string`'s own move constructor. The move assignment operator does the same handover, but over an object that already exists. Both are marked `noexcept`; this mark is not just a documentation note — standard library containers like `std::vector` decide whether they can move their elements during reallocation by looking at this mark. A move constructor that is not `noexcept` can force a container to copy instead of move.

```cpp
// box.cpp — move constructor, move assignment, and the source's destructor
#include <cstdio>
#include <string>
#include <utility>
class Box {
public:
    explicit Box(std::string label) : label_(std::move(label)) {}
    Box(Box&& other) noexcept : label_(std::move(other.label_)) {
        std::printf("move constructor ran\n");
    }
    Box& operator=(Box&& other) noexcept {
        label_ = std::move(other.label_);
        std::printf("move assignment ran\n");
        return *this;
    }
    Box(const Box&) = delete;
    Box& operator=(const Box&) = delete;
    ~Box() { std::printf("Box destructor ran\n"); }
private:
    std::string label_;
};
static void move_functions() {
    Box a("first label, a text long enough to go past the small string optimization");
    Box b(std::move(a));
    Box c("second label, again a text long enough to go past the small string optimization");
    c = std::move(b);
}
static void string_move() {
    std::string s = "a text long enough to go past the small string buffer, for a move";
    std::string t = std::move(s);
    std::printf("t (the value coming from the source) = %s\n", t.c_str());
    s = "new value";
    std::printf("s (after a new value is given) = %s\n", s.c_str());
}
static void const_string() {
    const std::string c = "a constant text, too constant to bind to a move constructor";
    std::string before = c;
    std::string copy_obj = std::move(c);
    std::printf("copy_obj (the value coming from c) = %s\n", copy_obj.c_str());
    std::printf("c unchanged: %s\n", (c == before) ? "yes" : "no");
}
int main() {
    move_functions();
    string_move();
    const_string();
}
```

```text
move constructor ran
move assignment ran
Box destructor ran
Box destructor ran
Box destructor ran
t (the value coming from the source) = a text long enough to go past the small string buffer, for a move
s (after a new value is given) = new value
copy_obj (the value coming from c) = a constant text, too constant to bind to a move constructor
c unchanged: yes
```

**OM3.** None of the `Box` objects inside `move_functions` are left without a destructor for having been moved: every `Box` object that is opened prints its own `Box destructor ran` line at the end of scope. Even after `a`'s content is moved into `b`, and `b`'s into `c`, all of them are destroyed normally at the end of the function. This confirms the previous lesson's M10/K02 and M11/K02 comparison again: in Rust, a moved variable is treated by the compiler as no longer usable, and no destruction code runs for it at scope end; in C++, the moved object keeps living, its destructor still runs — a move affects the object's content, not its lifetime.

Another subtlety of the move assignment operator is its behavior toward self-assignment: a call like `x = std::move(x);` should be writable and must not corrupt the object. `Box`'s move assignment operator relies here on `std::string`'s own move assignment through the line `label_ = std::move(other.label_);`; `std::string`'s own self-move-assignment is safe: it does not lead to undefined behavior and leaves the object valid, but the standard does not say which value the string will hold afterward; the result falls into this lesson's **unspecified** kind. When writing a class that manages a resource by hand (say, a class holding a raw pointer), this check has to be added explicitly — otherwise self-assignment can release the object's resource and then immediately try to read that released resource.

## Why the Source's Destructor Still Runs

Every `Box destructor ran` line in the `move_functions` output is not a random detail; it is a direct consequence of C++'s move model. Once an object is moved, it keeps staying in scope — neither its lifetime shortens nor is its destructor call skipped. Even after `a` hands its content to `b`, `a` is still a valid `Box` object; when scope ends, the compiler still generates the normal destructor call for it too. The destructor itself makes no distinction between having been moved and not having been moved — `~Box()` always runs the same body, because the class author writes the destructor only once, for the object's ordinary end.

The condition for this behavior to be safe is that the move constructor and move assignment leave the source in a state the destructor can safely run against. Here `Box` hands off its `label_` member to `std::string`; `std::string`'s own move constructor puts the source string into a valid but unspecified state and guarantees that this state can be safely released by its own destructor. If `Box` held a raw `char*` instead of a `std::string`, the move constructor would have had to null out the source's pointer — otherwise both the source and the target would try to release the same memory, and the program would fall into a double-free error. This is the implicit rule of writing a move constructor: leave the source in a state where that source's destructor can run a second time without doing harm.

This is exactly where it parts ways with Rust's model. In Rust, the compiler generates no destruction code at all for a moved variable at scope end — since the variable is no longer counted as "alive," a drop call is never applied to it. In C++ there is no such exemption: the moved object keeps living until the end of scope, and its destructor, just like that of an object never moved, is guaranteed to run. This is why it matters in C++ to know which kind (**unchanged**, **changed**, **empty**, **unspecified**) the source falls into after a move — the object does not disappear, and the rest of the code will still encounter it (or at least its destructor).

## A Moved `std::string`: A Valid But Unspecified State

After the line `std::string t = std::move(s);` in the `string_move` function, `s`'s content is never read anywhere; it is directly overwritten with `s = "new value";`. This ordering is not arbitrary: the standard says a moved `std::string` remains in a "valid but unspecified state" — the object is still valid, calling `size()` on it or assigning it a new value is safe, but what it holds at that moment is not defined by the standard. That is why this lesson never prints `s`'s content after the move; it only reads its state after a new value has been given.

`t`'s value is different: the destination object's content after a move is fully defined by the standard — it will be identical to the source's value before the move. That is why the `t (the value coming from the source) = ...` line in the output can be printed safely; only the source falls under **unspecified**, not the destination.

**OM4.** This is also where the reason a move is cheaper than a copy is hidden: `std::string`'s move constructor takes over `t`'s buffer pointer from `s`, it does not copy the characters one by one. If a copy constructor were called instead, a new buffer would be allocated for every character in the source, and data would be written into it one character at a time from the source — an operation proportional to the string's length. The move constructor, on the other hand, only copies a handful of members (the buffer pointer, the length, the capacity) into the destination and zeroes out the source's own — a fixed-cost operation independent of how long the string's content is. This lesson does not count this handover — how many characters were moved or how many bytes were not copied is not what this lesson measures; the only thing measured is which kind the source falls into once the operation finishes.

The "which object" question the previous course measured (M11/K04) does not apply here: there, the **copy** kind carried a destination's name, here **unchanged** carries a source's own name. A move always establishes the source's old value at the destination — the destination's kind is not the subject of this measurement; what is measured is the source that is left behind.

## The Bounding Measurement

The `const_string` function shows this lesson's boundary: since `c` is a `const std::string`, the call `std::move(c)` produces an rvalue reference of type `const std::string&&`. `std::string`'s move constructor expects a `std::string&&` (non-const) parameter; `const std::string&&` cannot bind to it. In this case the compiler falls back in overload resolution and calls the copy constructor. The result: `c` stays **unchanged** even after a call that looks like a move — the `c unchanged: yes` line in the output confirms this.

This is the clearest example showing that writing `std::move` does not guarantee a move. `std::move` is only a type conversion; which constructor actually gets called is decided by the compiler's overload resolution, and a `const` qualification can steer that resolution away from the move constructor. In practice this means passing a `const` variable into a function with the intent of moving it is usually a design mistake: as long as the variable stays `const`, it is not possible for the compiler to pick the move version, the code always falls back to the copy version — so if a resource is really meant to be moved, that resource must not be `const`.

## Summary

- An lvalue is a named expression whose address can be taken; an rvalue is a temporary result; an rvalue reference (`T&&`) binds only to rvalues.
- `std::move` is a type conversion; it moves no data itself — which constructor gets called is decided by overload resolution.
- A moved `std::string`'s source is in a valid but unspecified state (the **unspecified** kind); this lesson always gave it a new value before reading its content.
- The moved object's destructor keeps running normally — in C++, a move does not end a lifetime, it only hands over content.
- Calling `std::move` on a `const` source falls to the copy constructor, not the move constructor; the source stays **unchanged** in that case — this is this lesson's bounding measurement.

## Next Step

This lesson showed that whether a move actually happens is decided not by writing `std::move` but by which constructor is actually called. Whether a class's move constructor is implicitly written by the compiler at all depends on a similar resolution — and even another special member function the class writes can break that resolution. The next lesson measures how writing a destructor suppresses the implicit move constructor, and how the rule of zero and the rule of five manage this.
