---
title: 'Resource Management and the Standard Library'
source: 'https://academia.sh/en/courses/cpp-standard-library'
language: en
updated: '2026-09-26T03:02:46+00:00'
license: 'CC BY-SA 4.0'
lessons: 11
outcomes:
    - 'Expresses ownership semantics in the type with smart pointers'
    - 'Explains how move semantics removes the cost of a copy'
    - 'Chooses containers and algorithms with the right complexity'
    - 'Tells whether an operation leaves the source unchanged, changes it, empties it or leaves it in an unspecified state, and measures it'
    - 'Recognizes the places where a moved-from object and an invalid iterator may only be given a new value'
---

# Resource Management and the Standard Library

In C++, when an operation finishes, the object that was its source lives on: the course measures every mechanism with a single question — what does this operation leave in the source; did the source stay unchanged, did it change, was it emptied, or does the standard say nothing about its state at all? Smart pointers, move semantics, the rule of zero and the rule of five, containers, iterators, algorithms, lambdas, ranges and type deduction all pass through the same frame.

## Ownership and Move Semantics

Smart pointers, move semantics and rvalue references, the rule of zero and the rule of five, non-copyable and non-movable types — each paired with the question of what an operation leaves in the source.

### Smart Pointers

Source: https://academia.sh/en/courses/cpp-standard-library/smart-pointers

Naming what an operation leaves in the source after it runs on a unique_ptr, shared_ptr, or weak_ptr with four kinds: unchanged, changed, empty, unspecified.

The previous course measured which object an expression works with, and closed by noting that copying is not the only path: an operation can take over what the source holds instead of opening a new copy at its target. This handover is called a move. When a move finishes, the source object does not vanish — its destructor still runs — but what it holds afterward now depends on the operation. This course names what is left in the source after an operation with four kinds; this lesson introduces those four kinds through smart pointers.

#### Four Kinds and the Full Run of the Core

The three files below build the shared measurement tool for this course's eleven lessons. `leftover.hpp` names the four kinds:

```cpp
// leftover.hpp — a core that names what is left in the source after an operation
#ifndef LEFTOVER_HPP
#define LEFTOVER_HPP
#include <string>
enum class Left { unchanged, changed, empty, unspecified };
// Turns a before/after observation into a kind. Emptiness is checked before equality.
Left compare(bool stayed_same, bool emptied);
// Prints the operation name and the kind on one line, and bumps the kind counter.
void report(const std::string& op, Left left);
// An unspecified source is not looked at: it first gets a new value or is destroyed.
template <typename NewValue>
void unspecified(const std::string& op, NewValue new_value) {
    new_value();
    report(op, Left::unspecified);
}
// Prints the kind counts and the upper split.
void summary();
#endif
```

`leftover.cpp` ties the four kinds to a line format and a counter:

```cpp
// leftover.cpp — classification, line format and counter
#include "leftover.hpp"
#include <cstdio>
namespace {
int counts[4] = {0, 0, 0, 0};
const char* const names[4] = {"unchanged", "changed", "empty", "unspecified"};
}
Left compare(bool stayed_same, bool emptied) {
    if (emptied) return Left::empty;
    return stayed_same ? Left::unchanged : Left::changed;
}
void report(const std::string& op, Left left) {
    int i = static_cast<int>(left);
    ++counts[i];
    std::printf("%-32s %s\n", op.c_str(), names[i]);
}
void summary() {
    std::printf("kinds: unchanged %d, changed %d, empty %d, unspecified %d\n",
                counts[0], counts[1], counts[2], counts[3]);
    std::printf("state specified %d, not specified %d\n",
                counts[0] + counts[1] + counts[2], counts[3]);
}
```

The rest measures eleven operations and this lesson adds its own smart pointer examples:

```cpp
// left_behind.cpp — the core's full run plus a smart pointer deep dive
#include <algorithm>
#include <map>
#include <memory>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
#include "leftover.hpp"
static void smart() {
    auto p = std::make_unique<int>(7);
    int* before = p.get();
    auto q = std::move(p);
    report("unique_ptr moved", compare(p.get() == before, p == nullptr));
}
static void moving() {
    std::string s = "a text long enough to go past the small string buffer";
    std::string t = std::move(s);
    unspecified("std::string moved", [&] { s = "new value"; });
}
struct Record {
    std::string name;
    ~Record() {}
};
static void rule_of_five() {
    Record a{"the long name of a class with a written destructor"};
    std::string before = a.name;
    Record b = std::move(a);
    report("std::move on class with dtor", compare(a.name == before, a.name.empty()));
}
class Handle {
public:
    explicit Handle(int id) : id_(id) {}
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
    Handle(Handle&& other) noexcept : id_(other.id_) { other.id_ = -1; }
    Handle& operator=(Handle&&) = delete;
    int id() const { return id_; }
private:
    int id_;
};
static void non_copyable() {
    Handle a(3);
    int before = a.id();
    [[maybe_unused]] Handle b = std::move(a);
    report("own move constructor", compare(a.id() == before, a.id() == -1));
}
static void container() {
    std::map<std::string, int> stock{{"apple", 4}};
    auto before = stock;
    int count = stock["pear"];
    (void)count;
    report("operator[] read on a map", compare(stock == before, stock.empty()));
}
static void iterator() {
    std::vector<int> v{1, 2, 3, 4};
    auto it = v.begin() + 1;
    auto next_it = v.erase(it);
    unspecified("erased iterator", [&] { it = next_it; });
}
static void algorithm() {
    std::vector<int> v{5, 1, 4, 2};
    auto before = v;
    std::sort(v.begin(), v.end());
    report("std::sort", compare(v == before, v.empty()));
}
static void erase_remove() {
    std::vector<int> v{1, 2, 3, 2, 5, 2};
    auto new_end = std::remove(v.begin(), v.end(), 2);
    unspecified("tail after std::remove", [&] { v.erase(new_end, v.end()); });
}
static void lambda() {
    int step = 0;
    auto count_up = [step]() mutable { return ++step; };
    count_up();
    count_up();
    report("mutable capture by value", compare(step == 0, false));
}
static void range_view() {
    std::vector<int> v{1, 2, 3};
    auto before = v;
    int total = 0;
    for (int n : v | std::views::transform([](int x) { return x * 10; })) total += n;
    (void)total;
    report("view over an lvalue", compare(v == before, v.empty()));
}
static void type_deduction() {
    std::vector<int> v{1, 2, 3};
    auto before = v;
    for (auto& x : v) x *= 2;
    report("loop with auto&", compare(v == before, v.empty()));
}
static void shared() {
    auto p = std::make_shared<int>(9);
    auto before = p.get();
    auto copy_obj = p;
    report("shared_ptr copy", compare(p.get() == before, p == nullptr));
    std::printf("use count: %ld\n", static_cast<long>(p.use_count()));
    auto moved = std::move(p);
    report("shared_ptr moved", compare(p.get() == before, p == nullptr));
}
static void weak_reference() {
    auto p = std::make_shared<int>(21);
    std::weak_ptr<int> w = p;
    bool reachable_before = !w.expired();
    p.reset();
    bool reachable_after = !w.expired();
    std::printf("weak reference access: before %s, after %s\n",
                reachable_before ? "yes" : "no",
                reachable_after ? "yes" : "no");
}
static void move_only() {
    auto p = std::make_unique<int>(5);
    int* before = p.get();
    auto&& r = std::move(p);
    (void)r;
    report("p after std::move alone", compare(p.get() == before, p == nullptr));
}
int main() {
    smart(); moving(); rule_of_five(); non_copyable(); container();
    iterator(); algorithm(); erase_remove(); lambda(); range_view(); type_deduction();
    summary();
    shared();
    weak_reference();
    move_only();
}
```

When these three files are compiled and run together, this is printed:

```text
unique_ptr moved                 empty
std::string moved                unspecified
std::move on class with dtor     unchanged
own move constructor             empty
operator[] read on a map         changed
erased iterator                  unspecified
std::sort                        changed
tail after std::remove           unspecified
mutable capture by value         unchanged
view over an lvalue              unchanged
loop with auto&                  changed
kinds: unchanged 3, changed 3, empty 2, unspecified 3
state specified 8, not specified 3
shared_ptr copy                  unchanged
use count: 2
shared_ptr moved                 empty
weak reference access: before yes, after no
p after std::move alone          unchanged
```

**OM1.** The first thirteen lines define what this course measures: eleven distinct operations, each answering one question — what is left in the source once the operation finishes? The answer is one of four kinds: the source can stay **unchanged**, come out **changed** in a way the standard specifies, become **empty** by a guarantee the standard or the class itself gives, or fall into an **unspecified** state the standard says nothing about. The distribution across the eleven operations — 3 unchanged, 3 changed, 2 empty, 3 unspecified — stays the same from this lesson to the course's closing; each lesson deepens its own slice of these eleven with its own topic.

#### The Moved `unique_ptr`: What the Standard's Word Actually Guarantees

The first line of the output above reads `unique_ptr moved empty`. This is a promise the standard makes explicitly for `std::unique_ptr`: when a `unique_ptr` is moved into another `unique_ptr` through the move constructor or move assignment, the source object holds `nullptr`. This shows exactly where a smart pointer differs from a raw pointer: a raw pointer gives no guarantee at all about what it holds after being moved, but `unique_ptr` does.

This guarantee holds only for the pointer itself, not for the content of the object it points to. Trying to access `*p` on a moved `unique_ptr` — since `p` already holds `nullptr` — dereferences a null pointer, and that is undefined behavior. This lesson neither writes nor runs such a call; the defined counterpart is asking about emptiness first, with `if (p)`. The negation of the `w.expired()` check in the `weak_reference` function is another instance of the same discipline: instead of reaching directly into a resource that is not held, it asks first whether it is reachable.

#### Unique, Shared, and Weak Ownership

`std::make_unique` and `std::make_shared`, as seen in the code above, are the standard way to construct a smart pointer — preferred over writing a raw `new` call directly. The ownership the three carry differs. `unique_ptr` carries unique ownership: only one `unique_ptr` can hold an object at a time, so it cannot be copied, only moved. `shared_ptr` carries shared ownership: several `shared_ptr` instances can point to the same object, and a reference count tracks how many do.

The `shared` function shows this difference through two operations. First `copy_obj = p;` opens a copy; the `shared_ptr copy unchanged` line in the output says the source (`p`) still points to the same object after the copy — `p.get()` did not change, `p` is still not `nullptr`. The `use count: 2` printed right after confirms that the reference count rose along with the copy. Then `auto moved = std::move(p);` runs; this time the `shared_ptr moved empty` line shows that moving gives a result completely different from copying — the source is now `nullptr`.

**OM2.** Two different operations on the same class (`shared_ptr`) fall into two different kinds: copying leaves the source **unchanged**, moving leaves it **empty**. This confirms once more that what this course measures is not the class itself but the operation run on it.

#### Weak Reference and Reference Cycle

The third form of ownership is weak ownership: `std::weak_ptr` points to an object but does not raise the reference count, and so does not extend the object's lifetime. The `weak_reference` function shows this: `w` holds a weak reference to the object `p` points to; before the `p.reset()` call, `w.expired()` is false (the object is still alive); after the call, it is true (the object is now gone). The `weak reference access: before yes, after no` line in the output shows exactly this.

The reason a `weak_ptr` exists is to stand ahead of a reference cycle that would form with `shared_ptr`. If two objects hold each other with `shared_ptr`, neither reference count ever drops to zero, and neither object is ever destroyed — this is a memory leak. This lesson does not build and run such a cycle; instead it runs its defined counterpart: making one end of the cycle a `shared_ptr` and the other a `weak_ptr`, closing off the leak from the start. The `w.lock()` call — not used directly in this lesson — is the standard way to safely obtain a temporary `shared_ptr` from a `weak_ptr`: it returns a populated `shared_ptr` if the object is still alive, and an empty one if it is gone.

The question M11/K02 measured — "who owns the memory" — gets a different answer depending on the language. In Rust (M10/K02), once a variable is moved, the compiler treats that variable as no longer usable and refuses to compile code that tries to use it — a moved variable can never be read again, and no destruction code runs for it at scope end either. In C++ the situation differs: the moved object keeps living, its destructor still runs; the compiler does not forbid using a moved object, it only leaves which value it now holds to be settled by this course's four kinds. Ownership is declared plainly through the smart pointer's type (unique, shared, weak), but what this course itself measures is not who holds ownership — it is what is left in the source itself once an operation finishes.

#### The Bounding Measurement

The `move_only` function shows this lesson's finest point: `std::move` alone moves nothing. The line `auto&& r = std::move(p);` only converts `p` into an rvalue (the name for this conversion comes later); it calls no move constructor or move assignment, because `r` is a reference and does not construct a new `unique_ptr`. The `p after std::move alone unchanged` line in the output confirms this — `p.get()` is still the same as before, `p` is still not `nullptr`.

What empties the source is not the `std::move` call itself; what empties it is a move constructor or move assignment actually being called on that converted value. The line `auto q = std::move(p);` in the `smart` function empties `p` because that line constructs a new `unique_ptr` (`q`), and this construction calls `unique_ptr`'s move constructor. In `move_only` there is no such construction; that is why the result falls into a different kind. This distinction returns in a later lesson, where it is shown that a write like `std::move` is nothing but a type conversion.

#### Summary

- What is left in the source after an operation is named with four kinds: **unchanged**, **changed**, **empty**, **unspecified**; this course's eleven lessons measure different slices of these four kinds.
- A moved `unique_ptr`'s source becomes `nullptr` — this is a promise the standard gives; dereferencing a moved pointer is undefined behavior, and its defined counterpart is the `if (p)` check.
- A `shared_ptr` copy leaves the source **unchanged** and raises the reference count; a `shared_ptr` move leaves the source **empty** — same class, different operation, different result.
- `weak_ptr` points to an object without raising the reference count, and provides a defined counterpart to a reference cycle leak.
- `std::move` alone moves nothing; only a move constructor or move assignment that is actually called empties the source — this lesson's bounding measurement is a `std::move` call that leaves the source **unchanged**.

#### Next Step

This lesson looked at the outcome of moving: when a `unique_ptr` is moved, the source empties, because the standard gives this promise for the pointer itself. But when the thing being moved is not a pointer but a type like `std::string` that carries its own data, the standard does not speak with the same certainty. The next lesson opens up rvalue, lvalue, and the move constructor to measure this less certain outcome.

### Move Semantics

Source: https://academia.sh/en/courses/cpp-standard-library/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.

### The Rule of Zero and the Rule of Five

Source: https://academia.sh/en/courses/cpp-standard-library/rule-of-zero-and-five

How writing a destructor suppresses the implicit move constructor, how the five special member functions depend on one another, and how the rule of zero reaches the same result without ever forming that dependency.

The previous lesson showed that `std::move` alone does not guarantee a move — the actual decision is made by overload resolution: a `const` source could not bind to the move constructor, so it fell back to the copy constructor. The same fall can happen for a non-`const` source too — this time not because of a type qualifier, but because of another special member function the class itself writes. This lesson measures how writing a destructor silently removes the implicit move constructor.

#### Writing a Destructor Suppresses the Move Constructor

```cpp
// record.cpp — how writing a destructor suppresses the implicit move constructor
#include <cstdio>
#include <string>
#include <utility>
struct Record {
    std::string name;
    ~Record() {}
};
static void destructor_class() {
    Record a{"the long name of a class with a written destructor, past the small string optimization"};
    std::string before = a.name;
    Record b = std::move(a);
    std::printf("b.name = %s\n", b.name.c_str());
    std::printf("a.name unchanged: %s\n", (a.name == before) ? "yes" : "no");
}
struct Package {
    std::string content;
    explicit Package(std::string c) : content(std::move(c)) {}
    ~Package() = default;
    Package(const Package&) = default;
    Package& operator=(const Package&) = default;
    Package(Package&&) = default;
    Package& operator=(Package&&) = default;
};
static void five_default() {
    Package a("the content of a package moved with all five functions explicitly requested, kept long");
    Package b = std::move(a);
    (void)b;
    std::printf("five functions explicitly requested, move ran\n");
}
struct PlainRecord {
    std::string name;
};
static void zero_rule() {
    PlainRecord a{"the long name of a plain record following the rule of zero, for a move"};
    PlainRecord b = std::move(a);
    std::printf("b.name = %s\n", b.name.c_str());
    a.name = "new value";
    std::printf("a.name (after a new value is given) = %s\n", a.name.c_str());
}
int main() {
    destructor_class();
    five_default();
    zero_rule();
}
```

```text
b.name = the long name of a class with a written destructor, past the small string optimization
a.name unchanged: yes
five functions explicitly requested, move ran
b.name = the long name of a plain record following the rule of zero, for a move
a.name (after a new value is given) = new value
```

**OM5.** `Record`'s only written special member function is the destructor — even with an empty body, the moment the user writes a destructor the compiler no longer generates an implicit move constructor or an implicit move assignment. The line `Record b = std::move(a);` therefore finds no move constructor; overload resolution falls back to the copy constructor, which still implicitly exists. The result is that `a.name` stays **unchanged** even after the move — the `a.name unchanged: yes` line in the output confirms this. `std::move(a)` was written here too, but what got called was not a move but a copy.

This looks like it reaches a different conclusion from the question the previous course measured (M11/K04: which object does an expression operate on), but it actually repeats the same distinction: in K04, the **copy** kind named a destination opening a new object; here, **unchanged** names the source itself staying the same. The line `Record b = std::move(a);` is a copy operation in the sense K04 measures (`b` is a new object), but the source's (`a`'s) state in the sense this course measures is **unchanged** — the two measurements evaluate the same line on two different axes.

#### The Five Special Member Functions and the Rule of Five

A class has five special member functions: the destructor, the copy constructor, the copy assignment operator, the move constructor, the move assignment operator. When one of these is written by hand, the implicit versions the compiler generates for the others can change or disappear entirely — the `Record` example above showed exactly that. The rule of five turns this dependency into a discipline: if one of these five is written by hand, the remaining four should also be considered, and written or requested explicitly if needed.

Each of the five runs at a different moment, for a different purpose. The destructor (`~T()`) is responsible for releasing every resource the object holds when its lifetime ends; if a class holds a raw resource (a file handle, a pointer, a lock), the destructor is the only guaranteed place to give that resource back. The copy constructor (`T(const T&)`) builds a new object as a copy of an existing one; it does not share the source, it opens its own independent copy. The copy assignment operator (`T& operator=(const T&)`) does the same copying work, but over a target object that already exists — so it must first release the target's old resource, then set up the new one. The move constructor (`T(T&&)`) and the move assignment operator (`T& operator=(T&&)`), on the other hand, take over the resource instead of copying it: without opening a new resource, they move what is in the source to the target and leave the source in a state the destructor can safely run against. This is exactly where the dependency among the five comes from — one is built on the assumption of another: the move constructor cannot safely empty the resource without knowing how the destructor releases it; the copy assignment operator cannot release the old resource without knowing which resource the destructor releases. When a class writes only one of these five by its own hand, the compiler can no longer confidently answer "were the rest also written under the same assumptions," and it stops generating some of them.

The `Package` class makes this request explicit with `= default`: the destructor, the copy constructor, the copy assignment operator, the move constructor, the move assignment operator — all five are requested from the compiler with `= default`. There is no gap here for the compiler to fill: `Package`'s own written constructor (`explicit Package(std::string c)`) only blocks the implicit **default** constructor (the parameterless `Package()`) from being generated; the other five special member functions would have been generated the same way even without writing `= default`. `= default` does not change the outcome, it makes the intent visible in the code: a reader sees that the five functions exist and run in their compiler-generated form without having to scan the file. The `five functions explicitly requested, move ran` line in the output states that the move constructor really is called here (and that it moves the `content` member in its compiler-generated form).

This rule is an extended version of K04's rule of three (rule of three — a copy constructor, a copy assignment operator, and a destructor depend on one another) with the move functions added; before C++11 only three functions depended on each other, and adding the move functions extended the dependency to five. K04's rationale was: if a class writes its own destructor (usually to release a raw resource), the compiler's generated implicit copy constructor and copy assignment operator copy that resource member by member — two objects end up sharing the same resource, and both try to release it in their own destructor, resulting in a double free. The same rationale applies to the move functions once added: if an implicit move constructor copies the resource from source to target and leaves it in the source too (without the zeroing specific to a move constructor), it carries the same double-free risk. The rule of five is therefore a continuation of the rule of three, not a separate rule: it is the same principle — "a hand-written special member function breaks the implicit assumption of the others" — extended to also cover moving.

#### Suppression Does Not Work the Same Way in Both Directions

The `Record` example showed one direction: writing a destructor prevents the move functions from being **generated** — there is no move constructor or move assignment, as if they had never been declared. Overload resolution sees this absence and falls back to the still-implicitly-generated copy constructor; that is why `Record b = std::move(a);` compiles and runs, it just performs a copy.

The reverse direction works differently. If a class writes its move constructor or move assignment by hand, the compiler does not implicitly **generate** the copy constructor and copy assignment — moreover, it treats them as explicitly `= delete`d. Code that tries to copy such a class does not compile; overload resolution does not find a fallback and quietly fall to it, it produces an error. This is exactly why `Package` explicitly requests all five with `= default`: had it written only the move functions and left copying to itself, any code trying to copy `Package` would be rejected at compile time — this may or may not be the outcome the class intends, but it is not meant to happen silently.

This asymmetry between the two directions is not arbitrary, it is a safety choice. When the compiler sees a move function written, it assumes the class author thought something special about how the resource is managed — perhaps the class considers it meaningless or dangerous for more than one object to share the same resource. Generating an implicit copy constructor for such a class (by copying the resource member by member) could silently violate the class's intent, leading to an error such as two objects holding the same raw resource and both trying to release it. The compiler avoids this risk by closing off copying entirely and waiting for the class author to decide explicitly. When a destructor is written, the same assumption does not hold — writing a destructor alone does not mean "this class cannot be moved," so the compiler does not close off copying, it only stops generating the move functions implicitly and falls back to the copying it finds safer.

#### The Rule of Zero

The rule of zero reaches the same goal by the opposite road from the rule of five: the class writes no special member function by hand at all, leaving the resource to members (such as `std::string`, `std::unique_ptr`, `std::vector`) that already manage their own five correctly. `PlainRecord` shows this: its only member is a `std::string`, and it has no special member function of its own. The compiler implicitly generates a move constructor that calls its member's move constructor — and this generated move constructor performs a real move.

This is why the rule of zero is preferred in practice: `Package`'s five `= default` lines add no new behavior, they only explicitly request what the compiler would generate anyway — without writing these lines at all, leaving the `content` member to another resource-managing member instead of a direct `std::string` (say, a `std::unique_ptr<std::string>`) would have been a way to get the same five functions implicitly from the compiler, without writing a single line. `Package` had to request the five by hand here because it wrote its own constructor (`explicit Package(std::string c)`); a class following the rule of zero is a class that also writes none of its own constructors by hand. A class delegating its resource management to its members is considered safer than the rule of five both because it takes less code and because it entirely removes the risk of manually setting up the dependency among the five functions correctly.

**OM6.** But the result of a real move falls into a different kind than `Record`'s. `b.name` takes on the source's old value (the destination's value is always defined), but `a.name` is now subject to `std::string`'s own post-move rule: a valid but unspecified state, that is, **unspecified**. This lesson assigns `"new value"` to `a.name` before reading it — the last line in the output shows the value after that assignment, not the raw post-move content.

#### The Bounding Measurement

Placing these three examples side by side shows this: a line that looks the same (`X b = std::move(a);`) can reach three different outcomes depending on how the class is written. For `Record` the result is **unchanged** (because it falls to the copy constructor), for `PlainRecord` the result is **unspecified** (a real move, but subject to the member's own valid-but-unspecified-state rule). Removing the destructor — taking the destructor out of `Record` — moves the class into `PlainRecord`'s situation, that is, from unchanged to unspecified. Which one is wanted is not a matter of right or wrong; it is a decision the class author makes in response to questions like whether the class should be movable at all and how cheap a move should be. This lesson does not show which way that decision should fall, only how the decision's outcome can be observed.

The concrete mechanics of this shift are as follows: `Record`'s body is no different from `PlainRecord`'s except for an empty destructor — both hold a single `std::string` member. The only difference is that `Record` writes the destructor by hand and `PlainRecord` never writes one. This one-line difference completely changes which constructor the call `X b = std::move(a);` falls into: in `PlainRecord` the compiler generates the move constructor, the source (`a.name`) is really moved and falls into the **unspecified** kind; in `Record` the move constructor is never generated, the call falls to the copy constructor, and the source stays **unchanged**. Adding or removing a destructor on a class, with no other line changed, can flip the same call's result from unchanged to unspecified or from unspecified to unchanged — among the four classes followed through this lesson, this is the one example where a class change is triggered this indirectly (by adding/removing a function that looks like it has nothing to do with moving at all).

#### Summary

- Writing a destructor by hand prevents the compiler from generating an implicit move constructor and move assignment; in that case move calls fall to the copying version and the source stays **unchanged**.
- The five special member functions (destructor, copy constructor, copy assignment, move constructor, move assignment) depend on one another; the rule of five recommends reviewing all of them when one is written, requesting them explicitly with `= default` if needed.
- The rule of zero writes no special member function at all, leaving the resource to members that already manage it correctly (such as `std::string`); in that case the compiler-generated move constructor performs a real move.
- Once a class following the rule of zero has its member moved, the source falls into the **unspecified** kind — a new value must be assigned before it is read.
- The choice between unchanged and unspecified is the class author's decision; this lesson's bounding measurement shows that the same line (`X b = std::move(a);`) can fall into two different kinds depending on how the class is written.

#### Next Step

This lesson implicitly showed that a source staying empty after a move can be either a promise the standard gives (as in the `unique_ptr` example) or a behavior the class writes by hand itself. The next lesson takes up this second path explicitly: the resource of a class that writes its own move constructor empties by a promise the class itself gives, not the standard.

### Non-Copyable and Non-Movable Types

Source: https://academia.sh/en/courses/cpp-standard-library/non-copyable-and-non-movable-types

Using = delete to forbid copying, moving, or both on a class; a resource emptying by a promise that comes from the class itself, not the standard.

The previous lesson showed that a resource's post-move state is sometimes decided by the standard (`std::string`), and sometimes by the class itself (`Record`, `PlainRecord`). This lesson turns to the last kind, **empty**: just as the standard gives the nullptr promise for `unique_ptr`, a class's own move constructor can also empty a resource explicitly and predictably. It also covers how `= delete` can close off copying and moving independently of each other.

The three lessons so far assumed a class was copyable and movable; the only question was which kind the post-move resource fell into. But for some classes, copying or moving itself makes no sense at all — copying a file handle can lead to two independent close calls, sharing ownership of a lock can lead multiple threads to believe they hold the same lock. This lesson covers `= delete`, the tool a class author uses to close off copying, moving, or both entirely, and the three combinations of that closure (non-copyable-but-movable, copyable-but-non-movable, neither).

#### Restricted Ownership with `= delete`

```cpp
// handle.cpp — the cases arising from partially deleted special member functions
#include <cstdio>
#include <type_traits>
#include <utility>
class Handle {
public:
    explicit Handle(int id) : id_(id) {}
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
    Handle(Handle&& other) noexcept : id_(other.id_) {
        other.id_ = -1;
    }
    Handle& operator=(Handle&&) = delete;
    int id() const { return id_; }
private:
    int id_;
};
static void handle_properties() {
    std::printf("Handle copy-constructible: %s\n", std::is_copy_constructible_v<Handle> ? "yes" : "no");
    std::printf("Handle move-assignable: %s\n", std::is_move_assignable_v<Handle> ? "yes" : "no");
    std::printf("Handle move-constructible: %s\n", std::is_move_constructible_v<Handle> ? "yes" : "no");
}
static void handle_move() {
    Handle a(7);
    Handle b(std::move(a));
    std::printf("b.id() = %d\n", b.id());
    std::printf("a.id() (after the move) = %d\n", a.id());
}
class CopyOnly {
public:
    explicit CopyOnly(int value) : value_(value) {}
    CopyOnly(const CopyOnly&) = default;
    CopyOnly& operator=(const CopyOnly&) = default;
    CopyOnly(CopyOnly&&) = delete;
    CopyOnly& operator=(CopyOnly&&) = delete;
private:
    int value_;
};
static void copy_only_properties() {
    std::printf("CopyOnly copy-constructible: %s\n", std::is_copy_constructible_v<CopyOnly> ? "yes" : "no");
    std::printf("CopyOnly move-constructible: %s\n", std::is_move_constructible_v<CopyOnly> ? "yes" : "no");
}
class Lock {
public:
    Lock() = default;
    Lock(const Lock&) = delete;
    Lock& operator=(const Lock&) = delete;
private:
    int state_ = 0;
};
static void lock_properties() {
    std::printf("Lock copy-constructible: %s\n", std::is_copy_constructible_v<Lock> ? "yes" : "no");
    std::printf("Lock move-constructible: %s\n", std::is_move_constructible_v<Lock> ? "yes" : "no");
}
int main() {
    handle_properties();
    handle_move();
    copy_only_properties();
    lock_properties();
}
```

```text
Handle copy-constructible: no
Handle move-assignable: no
Handle move-constructible: yes
b.id() = 7
a.id() (after the move) = -1
CopyOnly copy-constructible: yes
CopyOnly move-constructible: no
Lock copy-constructible: no
Lock move-constructible: no
```

**OM7.** `Handle` explicitly closes off its copy constructor and copy assignment with `= delete` — code trying to use either fails to compile. The move constructor, on the other hand, is written by hand: it moves the source's `id_` member into the target and writes `-1` into the source. This comes from a different place than `unique_ptr`'s nullptr promise: the one that empties `unique_ptr` is the standard itself, while the one that empties `Handle` is a value the class's author chose (`-1`). Both fall into the **empty** kind, but the party making the promise differs — one is the standard, the other is the class. The move assignment operator, meanwhile, is also closed off with `= delete`: `Handle` can be constructed by moving but cannot be move-assigned onto an existing object. This asymmetry can be intentional — for instance, `Handle` might wrap a resource for which reassignment after initial construction makes no sense.

`Handle`'s being non-copyable but movable is not a random combination; it is exactly the same pattern the standard library's `unique_ptr` follows. `unique_ptr` also closes off its copy constructor and copy assignment with `= delete`, writes its move constructor and move assignment by hand, and hands over the resource (the raw pointer it holds) from source to target, leaving `nullptr` in the source. `Handle` pulling its `id_` member to `-1` is a small-scale repetition of `unique_ptr` pulling its pointer to `nullptr`: both enforce the rule "this resource can only have one owner at a time, but ownership can be transferred from one object to another" by forbidding copying entirely and explicitly allowing moving. This is the concrete method a class author uses when they want a `unique_ptr`-like ownership model: delete the copying functions, write the moving functions by hand, and pull the source to a predictable value in the source.

#### Copyable But Non-Movable: A Restriction in Reverse

`CopyOnly` becomes non-movable by a different route than the previous lesson's `Record`. `Record` never declared its move functions at all; when a move was attempted, overload resolution silently fell back to the copy constructor, and the program compiled and ran. `CopyOnly`, on the other hand, **declares and deletes** its move functions. The difference shows up when a move is attempted: a deleted function fully participates in overload resolution, it is a candidate; if it is the best-matching candidate, the program does not compile. A line trying to initialize a `CopyOnly` object with `std::move` therefore does not silently fall to a copy as in `Record`'s case, it produces a straight compile error. This lesson does not place that line in a separate code block — because a block that fails to compile would violate this course's rule that every block is really compiled and run — but `is_move_constructible_v<CopyOnly>` printing `no` is indirect proof that this ban is real.

The distinction here is: if a move function does not exist at all, overload resolution falls back to another candidate that does exist (the copy constructor) — a silent, compiling result. If a move function exists but is deleted, overload resolution picks it as the best match and stops there — a noisy, non-compiling result. Both can be described as "non-movable," but one silently routes the program to a different operation, the other keeps the program from compiling at all.

`CopyOnly`'s copyable-but-non-movable combination is the reverse of `Handle`'s, and is usually chosen for a different reason: if a class holds a resource whose value can be shared but whose ownership cannot be transferred (say, a fixed configuration value, or a small piece of data that is cheap to copy but whose moving carries no extra meaning), closing off the move functions and allowing only copying can be a deliberate design decision. This is clearly visible in the `CopyOnly` example, where the `value_` member is an `int`: `int` is a movable type, the class closes off moving by its own choice, not because of its member's type. A class's movability, just like its copyability, follows from its own declared special member functions, not from its members' movability.

#### Neither Copyable Nor Movable

For some types, neither copying nor moving makes sense — copying a lock's ownership can lead to two independent locks, moving it can confuse which thread holds the lock. The standard library's `std::mutex` is an example of this kind (this lesson does not teach concurrency, it only names it): both its copy and move functions are deleted, it is neither copyable nor movable.

These three examples — `Handle` (non-copyable, movable), `CopyOnly` (copyable, non-movable), `std::mutex` and `Lock` (neither copyable nor movable) — are three different answers to the same question: which ownership transfer makes sense for this type? That answer never comes from the calling code, a compiler flag, or a runtime setting; it is written into the class's own definition, into its `= default` and `= delete` lines. To learn whether a class is copyable or movable, one does not need to read the code that uses that class — looking at the class's own declaration is enough. This means ownership restrictions are declared in the type: the restriction lives not in a document, a comment, or a usage convention, but in a declaration the compiler itself can check.

The `Lock` class shows a subtler point here: the copy constructor is declared by hand with `= delete`, but neither of the move functions is ever written — neither `= default` nor `= delete`. Yet `is_move_constructible_v<Lock>` prints `no`.

**OM8.** The reason for this is that the rule for implicitly generating the move functions does not look at whether the copy constructor is **deleted**, only at whether it is **declared**: if a class declares its copy constructor, its copy assignment, or its destructor as a user — even if that declaration is `= delete` — the compiler no longer implicitly generates the move functions. `Lock` never mentioned moving at all, but became non-movable indirectly by deleting its copy constructor. This also sharpens the distinction between **empty** and **unspecified**: in both classes a value is taken from the source and carried to the target — the source loses its pre-move value, both are the same in that respect. The distinction is whether it is stated where the value the source is left with ends up. **Empty** is the state where the value the source will take after a move is explicitly promised by the standard or by the class itself, that is, where it is stated where it ends up (`Handle`'s `-1`, a promise from its own move constructor; `unique_ptr`'s nullptr, a promise from the standard). **Unspecified** is the state where no such promise is ever given, where it is not stated where the source ends up (`std::string`'s moved content — it is valid, but which value it holds is not specified). `Lock`'s situation is neither of these — since moving is never possible at all, the question of the source's "post-move state" is never even asked.

#### The Bounding Measurement

`Lock`'s `int` member named `state_` is a movable type, and the class itself contains no other explicit move ban — by member-based reasoning one might say "of course it is movable." But the result is `no`. The reason is not the members' type but a single special member function `Lock` itself declares: the deleted copy constructor. The rule seen throughout this lesson appears here in its most compressed form: the question of movability does not look at the members, it looks at whether any of the class's "big three" (destructor, copy constructor, copy assignment) has been touched by the user. If it has been touched — even if deleted — moving is removed from implicit generation.

This rule shares the same root as the previous lesson's `Record` example, but reaches the opposite outcome. `Record` removed moving from implicit generation by writing a destructor; overload resolution fell back to the copy constructor that still implicitly existed — the result was a silently running copy. `Lock`, on the other hand, removes moving from implicit generation by **deleting** the copy constructor; but this time there is no copy constructor left to fall back to either, because it too is deleted. The result is a class that is neither copyable nor movable. The same underlying rule — a user-declared special member function suppresses the implicit generation of the others — lands in two different directions on two different classes: a silent copy in one, a total shutdown in the other. This is the direct link the fourth lesson forms with the third: the same mechanism can produce a wildly different outcome depending on which function is touched.

These three combinations — `Handle`, `CopyOnly`, `Lock` — correspond to a layer beneath the question followed through this course's four lessons (what an operation leaves in the source), namely the question "is this operation even possible for this resource at all." The previous three lessons assumed moving was always possible and measured its outcome; this lesson showed that moving (or copying) might not be possible at all, and that this impossibility, just like a move's outcome, is written into the class's own declaration. Someone reading a class's API can look at its `= default` and `= delete` lines and know in advance which transfers are possible with that class, which code pattern will fail to compile, which resource can be shared among how many objects — without needing to consult the class's documentation or usage guide.

#### Summary

- `= delete` can close off copying and moving independently of each other; `Handle` shows a move-constructible-but-not-move-assignable, non-copyable combination.
- A class's own move constructor can explicitly empty a resource (`Handle`'s `-1`); this is the same **empty** kind, but coming from a different source than a promise the standard gives (`unique_ptr`'s nullptr).
- If a move function is never declared, overload resolution silently falls back to the copy constructor (`Record`); if a move function is declared and deleted, the same call fails to compile (`CopyOnly`) — both are "non-movable" but their outcomes differ.
- A user-declared copy constructor — even a deleted one — keeps the compiler from implicitly generating the move functions; `Lock` becomes non-movable without ever mentioning moving.
- Some types (such as `std::mutex`) are designed to be neither copyable nor movable; this means ownership must never be transferred for that type by any means.

Recognizing these three patterns also helps when encountering a type from a third-party library or the standard library: `std::thread` is non-copyable but movable, so is `std::ifstream`; `std::atomic` is neither copyable nor movable. None of this behavior can be guessed without reading documentation, but where the class's `= delete` and `= default` lines are visible, it can be read directly from there.

#### Next Step

These four lessons measured the four possible outcomes of moving a single object (unchanged, changed, empty, unspecified) and where that outcome comes from — the standard, the class, or the operation's own impossibility. The next topic carries this question from a single object to a plural structure: how a container holds its elements, and which kind the container itself and its elements fall into when an element is taken out of one container and moved into another.

## The Standard Library

Containers, iterators and their validity rules, algorithms, the erase–remove idiom, lambdas, ranges and type deduction — each paired with the question of what an operation leaves in the source.

### Containers

Source: https://academia.sh/en/courses/cpp-standard-library/containers

The measurement of how a map's operator[] read inserts an element when the key is absent, and how find and contains leave the source unchanged.

The previous lesson, `Non-Copyable and Non-Movable Types`, showed a class
deciding for itself, by writing its own move constructor, what empty state it
leaves a moved-from source in — `Handle`'s id became `-1` because the move
constructor was written that way. This lesson turns to the standard
library's ready-made containers and asks the same question of a **read**
this time: writing `stock["pear"]` looks like reading a value. But if the
key `"pear"` is not in the map, what does this line leave in the source?

#### Reading with `operator[]` Is Actually Writing

`std::map<std::string, int>::operator[]` returns the value bound to a key
that is already in the map. If the key is absent, it does something
different: it **inserts** a new element with a zero-initialized value and
returns a reference to that element. This is a side effect that its
signature does not reveal — the square brackets of `[]` read as "query" to
most programmers, but for `map` they mean "get it if it is there, build it
if it is not."

```cpp
// container-read.cpp — does reading with operator[] insert into the map
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"apple", 4}};
    std::size_t before = stock.size();

    int count = stock["pear"];
    (void)count;

    std::printf("element count changed %s\n",
        (stock.size() != before) ? "yes" : "no");
    std::printf("pear value %d\n", stock["pear"]);
    return 0;
}
```

```text
element count changed yes
pear value 0
```

**SL1.** The call `stock["pear"]` **changed** the map while `"pear"` was
absent from it: the element count went up by one, and the new element's
value became `0`, `int`'s default value. The value assigned to `count`
looks like a read, but that read itself wrote into the map. The kind of
this expression is **changed** — the source still carries a value and the
standard states it (not an empty map, but a one-element map), but the
value differs from what it was before the operation.

#### `find` and `contains`: Asking the Same Question Without Inserting

There are two ways to learn whether a key is in the map that do not call
`operator[]`. `find` returns an iterator to the element if it finds the
key, or `end()` if it does not — it never inserts an element in either
case. `contains`, a C++20 addition, is the more direct route; it returns
only a `bool`.

```cpp
// container-query.cpp — do find and contains change the container
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"apple", 4}};
    auto before = stock;

    auto it = stock.find("pear");
    bool present = stock.contains("cherry");

    std::printf("find result %s\n", (it == stock.end()) ? "not found" : "found");
    std::printf("contains result %s\n", present ? "yes" : "no");
    std::printf("map changed %s\n", (stock != before) ? "yes" : "no");
    return 0;
}
```

```text
find result not found
contains result no
map changed no
```

**SL2.** `find("pear")` and `contains("cherry")` both queried keys that
were not in the map, and both left the map **unchanged**: `stock` still
equals its pre-operation copy. The `end()` iterator that `find` returns is
a way of saying "not here," not a trigger for insertion. Code that only
wants to check whether a key exists in a map should use `find` or
`contains` instead of `operator[]` — otherwise the query changes the
structure it is querying.

#### Container Families and the Criterion for Choosing

The standard library's containers fall into three families. **Sequence
containers** (`vector`, `deque`, `list`, `array`) take an element's
position from the order it was inserted in — "sequence" here does not mean
sorted by size, an element's place is wherever the programmer put it.
`vector` sits in contiguous memory, and appending at the end is amortized
constant time; access at an index is constant time. `list` is a doubly
linked list; inserting in the middle is constant time, but there is no
indexed access, only traversal from the start.

**Associative containers** (`map`, `set`) keep elements ordered by a key;
they are typically implemented as a balanced binary search tree, and
search, insertion and deletion are `O(log n)`. **Unordered containers**
(`unordered_map`, `unordered_set`) keep elements in a hash table; search
and insertion are `O(1)` on average, but fall to `O(n)` in the worst case
if every element lands in the same bucket. The choice depends on which
operation happens often: ordered traversal that runs often calls for
`map`; fast lookup where order does not matter calls for `unordered_map`.

```cpp
// family-choice.cpp — reading from a sequence, associative and unordered container
#include <cstdio>
#include <map>
#include <unordered_map>
#include <vector>

int main() {
    std::vector<int> sequence{30, 10, 20};
    std::map<int, int> associative{{1, 10}, {2, 20}};
    std::unordered_map<int, int> unordered{{1, 10}, {2, 20}};

    std::printf("vector first element %d\n", sequence.front());
    std::printf("map smallest key %d\n", associative.begin()->first);
    std::printf("unordered_map lookup %d\n", unordered.at(1));
    return 0;
}
```

```text
vector first element 30
map smallest key 1
unordered_map lookup 10
```

**SL3.** All three reads left a container **unchanged**: `front()`, the
`first` read through an iterator, and `at()` none of them wrote to the
source. The order `unordered_map` keeps its elements in was never queried
here, and never will be — an unordered container's traversal order is
implementation-defined and this course never prints it. `begin()->first`
on `map` gives `1` on every run because `map` is ordered by key; the same
call on an `unordered_map` would not give a reliable result.

#### `at()`: Access with Bounds Checking

`operator[]` has one more problem: it cannot be used on a `const` map,
because the possibility of inserting an element when the key is absent
makes it a mutating operation. `at()` removes this problem — it never
inserts an element when the key is absent, it throws `std::out_of_range`
instead, and it can be used on a `const` map.

```cpp
// at-access.cpp — what at() does with a key out of range
#include <cstdio>
#include <map>
#include <stdexcept>
#include <string>

int main() {
    const std::map<std::string, int> stock{{"apple", 4}};

    try {
        int value = stock.at("pear");
        (void)value;
        std::printf("caught no\n");
    } catch (const std::out_of_range&) {
        std::printf("caught yes\n");
    }

    std::printf("element count %zu\n", stock.size());
    return 0;
}
```

```text
caught yes
element count 1
```

**SL4.** `stock.at("pear")` threw an exception, and this lesson printed
only that it was **caught** — it did not print the exception's `what()`
text, because that text's exact form depends on the library implementation
and is not what this lesson measures. After the throw, `stock`'s element
count is still `1`: the map stayed **unchanged**. This is what makes
`at()` safe to use on a `const` container — an out-of-range key request
never changes the source at all, it only reports the request through an
exception.

#### `insert` and `emplace`: The Rule of Not Touching an Existing Key

`operator[]` does not just insert an element and return it when the key is
absent; if the key already exists, it **overwrites** the value. `insert`
and `emplace` carry the opposite guarantee: if the key already exists,
they change nothing, and they report through a `bool` that they did not
touch the existing element.

```cpp
// insert-emplace.cpp — does insert change the source when the key already exists
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"apple", 4}};

    auto result = stock.insert({"apple", 99});
    bool inserted = result.second;

    std::printf("inserted %s\n", inserted ? "yes" : "no");
    std::printf("apple value %d\n", stock.at("apple"));
    return 0;
}
```

```text
inserted no
apple value 4
```

**SL5.** The call `insert({"apple", 99})` ran while `"apple"` was already
in the map, and `result.second` returned `false` — no insertion took
place. The value of `apple` is still `4`: the map stayed **unchanged**
after this call, the value `99` was never written. This is a clear
difference from `operator[]` as a baseline: had `stock["apple"] = 99` been
written, the value would have been overwritten unconditionally and the
kind would have been **changed**. This rejecting behavior of `insert` is
the reason code that only wants to fill a map when a key is missing
chooses `insert` or `emplace` over `operator[]` — `emplace` carries the
same guarantee, differing only in that it builds the element directly in
the container's memory instead of constructing a temporary object first.

#### The Complexity of `erase` Depends on the Container

Deleting an element does not do the same work in every container.
`std::vector::erase` shifts every element after the deleted position one
slot to the left — the closer the deleted position is to the start, the
more elements get moved, and its complexity is `O(n)`. `std::list::erase`
holds no contiguous memory, so it does no shifting; it only updates the
links of the deleted node's neighbors, and its complexity is `O(1)`.
`std::map::erase` looks up by key and then removes a node from the tree,
`O(log n)` overall.

```cpp
// erase-complexity.cpp — does vector erase shift the remaining elements
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sequence{10, 20, 30, 40};

    sequence.erase(sequence.begin());

    std::printf("element count %zu\n", sequence.size());
    std::printf("first element %d\n", sequence.front());
    return 0;
}
```

```text
element count 3
first element 20
```

**SL6.** `sequence.erase(sequence.begin())` deleted the first element, and
the remaining three shifted one slot to the left — `front()` now gives
`20`, which used to sit second. The source **changed** after this
operation: not only did the element count drop by one, the remaining
elements' positions in memory changed too. Had the same call been made on
a `std::list`, the outcome's kind would still be **changed** — the
element count would still drop by one — but no shifting would happen; the
remaining elements' addresses would stay fixed. Because this lesson's
classification (unchanged/changed/empty/unspecified) measures not *how
much* work an operation does but *what state* it leaves the source in,
these two `erase` calls on `vector` and `list` fall under the same kind;
the complexity difference is a separate axis, and complexity and kind are
weighed together when choosing, neither one substitutes for the other.

#### Out-of-Range Indexing: Undefined Behavior and Its Defined Counterpart

`std::vector::operator[]` never checks whether the index it is given is
within the container's bounds. The call `sequence[10]` is **undefined
behavior** when `sequence`'s size is `3`: the runtime may produce a crash,
may return a garbage value, or may "work" without any error — all three
are equally possible, and this course shows none of them, because
undefined behavior's output is not defined by the standard and cannot be
verified. `std::vector::at()` gives the same request a **defined**
counterpart: it throws `std::out_of_range` if the index is out of range,
and accesses the same element as `operator[]` if the index is in range.
The difference is in performance — `at()` performs a bounds check on every
call, `operator[]` does not — but that difference is a visible cost only
for accesses within bounds; `operator[]`'s being "free" for an
out-of-range access comes at the price of the source's resulting state
now being undefined. `at()` is preferred everywhere an index's validity
cannot be guaranteed; in tight loops where the caller already guarantees
the index's validity, `operator[]`'s uninspected speed is accepted.

#### The Bounding Measurement

Writing `stock["pear"]` on `const std::map<std::string, int>& stock` **does
not compile**: `operator[]` has a non-`const` overload only, and it cannot
be called on a `const` object, because the operator's body can insert an
element when the key is absent — this conflicts with the `const`
contract. The block below carries that line as a **comment** and shows,
in its place, a version that does compile, one that reads a `const` map
only through `find`, `at` and `contains`:

```cpp
// const-map.cpp — why operator[] cannot be used on a const map
#include <cstdio>
#include <map>
#include <string>

int main() {
    const std::map<std::string, int> stock{{"apple", 4}};

    // The line below does not compile, because operator[] is not a const member:
    // int count = stock["pear"];

    auto it = stock.find("apple");
    std::printf("apple found %s\n", (it != stock.end()) ? "yes" : "no");
    std::printf("element count %zu\n", stock.size());
    return 0;
}
```

```text
apple found yes
element count 1
```

The compiler's rejecting this line is not a limitation, it is a guarantee:
it enforces, at compile time, that a `const` map can only be queried, that
no read can change it. The operation this lesson measures — a read with
`operator[]` on a mutable map — does not carry this guarantee; the path
that writes to the source is open, and the compiler does not close it.

#### Summary

- Reading a `std::map` with `operator[]` inserts a zero-initialized
  element when the key is absent; the kind of the operation this lesson
  measures is **changed**.
- `find` and `contains` never write to the map while searching for a key;
  the source stays **unchanged**.
- Sequence (`vector`, `deque`, `list`, `array`), associative (`map`,
  `set`) and unordered (`unordered_map`, `unordered_set`) containers
  carry different complexity guarantees (`erase`, for instance, is `O(n)`
  on `vector`, `O(1)` on `list`, `O(log n)` on `map`); but that difference
  does not affect the classification, all of them leave the source
  **changed**.
- `insert` and `emplace` write nothing when the key already exists — the
  source stays **unchanged**; writing with `operator[]` overwrites
  unconditionally, and the kind is **changed**.
- `at()` throws an exception when the key is absent and never changes the
  map, which is why it can be used on a `const` container too; by
  contrast, an `operator[]` call on a `const` map does not compile — the
  compiler closes off, from the start, a path that could change the
  source, through the `const` contract.
- `vector::operator[]` enters undefined behavior at an out-of-range index;
  `vector::at()` answers the same request with a defined exception — both
  go no further than reading the source, the only difference is whether
  the response to an out-of-range request is defined or not.

#### Next Step

This lesson measured what happens to a container as a whole. The next
lesson brings the boundary down to **a position inside** a container: can
an iterator still be read after the operation that deletes the element it
points to, or does touching that position enter undefined behavior?

### Iterators and Validity

Source: https://academia.sh/en/courses/cpp-standard-library/iterators

The measurement of how reading an iterator after erase enters undefined behavior, and how an iterator that stays valid remains unchanged.

The previous lesson measured that the call `stock["pear"]` inserts a
zero-initialized element into the map, and that this **changes** the
source. What changed there was the container as a whole: the element
count and the map's contents. This lesson brings the boundary down to a
smaller unit: an iterator that points to a **single position** inside a
container. When an element is deleted, what happens if an iterator that
pointed to that element is kept around? The answer to this question
differs from container to container — the same `erase` call invalidates
every following iterator on a `vector` but affects only the deleted
element itself on a `list` — so the question "is this iterator still
valid" can only be answered by looking not just at the operation's name
but at which container it was called on.

#### Reading an Iterator After `erase`

`std::vector::erase` removes the element at the given position and shifts
every element after it back by one position. The iterator to the removed
element is **invalid** after this operation — it either points to memory
that no longer belongs to the container, or to a position that another
element has since moved into. Reading this iterator is undefined
behavior, and this lesson **never runs it**; it only uses the new, valid
iterator that `erase` returns.

```cpp
// erase-returned-iterator.cpp — which iterator does erase leave valid
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{10, 20, 30, 40};
    auto it = numbers.begin() + 1; // points to 20

    auto next = numbers.erase(it);

    std::printf("element count after erase %zu\n", numbers.size());
    std::printf("value at the position erase returned %d\n", *next);
    return 0;
}
```

```text
element count after erase 3
value at the position erase returned 30
```

**SL7.** After the call `erase(it)`, the variable `it` is no longer valid
— this lesson never reads `it` again after the erase, because reading it
enters undefined behavior, and undefined behavior is never run in this
course. The `next` iterator that `erase` returns is used instead: the
standard guarantees that `erase` returns a new, valid iterator pointing to
the position after the removed element. The kind of this operation is
**unspecified**: what state `it` itself is left in after the operation is
not defined by the standard; the only guarantee is that it must not be
read.

#### Operations That Do Not Invalidate: Iterators That Stay Unchanged

Not every mutating operation invalidates iterators. `std::list::erase`
invalidates only the iterator to the deleted element — every other
iterator keeps its validity, because a linked list does not keep its
elements in contiguous memory, and deleting one node does not move the
other nodes.

```cpp
// list-erase-other-iterators.cpp — what happens to a non-deleted element's iterator
#include <cstdio>
#include <list>

int main() {
    std::list<int> values{10, 20, 30};
    auto middle = std::next(values.begin());       // 20
    auto last = std::next(values.begin(), 2);       // 30

    values.erase(middle);

    std::printf("value at the untouched iterator %d\n", *last);
    std::printf("remaining element count %zu\n", values.size());
    return 0;
}
```

```text
value at the untouched iterator 30
remaining element count 2
```

**SL8.** The `last` iterator pointed to a node that `erase(middle)` never
touched, and it keeps pointing to the same node after the call — its
value is still `30`. This iterator's state is **unchanged**: the standard
guarantees its validity because it was not the element that was deleted.
`std::vector` carries no such guarantee — when an element is deleted from
a `vector`, every element after it shifts back by one position, so every
iterator to a position after the deleted one also becomes invalid. This
difference shows that the choice of container directly affects how
iterators can be used.

#### Iterator Categories

The standard library sorts iterators into categories by functional
capacity. An **input iterator** can only read forward, in a single pass.
A **forward iterator** supports making more than one pass with the same
iterator. A **bidirectional iterator** (such as `std::list`'s iterator)
can move both forward and backward, but cannot jump in constant time. A
**random access iterator** (such as `std::vector`'s iterator) supports a
constant-time jump such as `it + n`.

```cpp
// iterator-category.cpp — the difference between random access and bidirectional
#include <cstdio>
#include <iterator>
#include <list>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    std::list<int> l{1, 2, 3, 4, 5};

    auto vit = v.begin() + 3; // constant-time jump
    auto lit = std::next(l.begin(), 3); // advances in linear time

    std::printf("value reached by jumping over the vector %d\n", *vit);
    std::printf("value reached by advancing over the list %d\n", *lit);
    return 0;
}
```

```text
value reached by jumping over the vector 4
value reached by advancing over the list 4
```

**SL9.** Both iterators reached the same result, but with different
complexity: `v.begin() + 3` is computed in constant time because it is
`vector`'s random access iterator; `std::next(l.begin(), 3)` is equivalent
to calling `++` three times on `list`'s bidirectional iterator, and
advances in linear time. This lesson does not measure the speed
difference — duration is never measured in this course — it only shows
that the two categories support different operations: writing `it + 3` on
a `list` iterator does not compile, because a bidirectional iterator does
not provide `operator+`.

#### Range-Based `for` and Implicit Iterators

A range-based `for` loop calls `begin()` and `end()` behind the scenes,
using iterators implicitly. If elements are inserted into or erased from
the container inside the loop body, the implicit iterators are subject to
the same validity rules — this is why calling `push_back` inside a
range-based `for` body over a `vector` can send the rest of the loop into
undefined behavior. This lesson never makes such a change.

```cpp
// range-for-read-only.cpp — how a range-based for traverses elements
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{5, 10, 15};
    int total = 0;

    for (int value : numbers) {
        total += value;
    }

    std::printf("total %d\n", total);
    std::printf("did the element count change %s\n",
        (numbers.size() == 3) ? "no" : "yes");
    return 0;
}
```

```text
total 30
did the element count change no
```

**SL10.** The loop only read each element and never wrote to the
container; `numbers` stayed **unchanged** at the end of the operation.
The implicit `begin()`/`end()` calls stayed valid throughout the loop for
the same reason — since the container's size and contents never changed,
no situation that would invalidate the iterators ever arose.

#### The Difference Between Empty and Unspecified

The `end()` iterator and an invalidated iterator are sometimes confused
for the same thing; both seem to mark a position that "should not be
read." But the standard attaches entirely different guarantees to the
two. `end()` is a value that is always defined and open to comparison,
denoting the position one past the container's last element — writing
`it == container.end()` is valid under every condition, because `end()`
itself is never invalid; only **dereferencing** it with `*end()` is
undefined. A container is **empty** when `begin() == end()` — this is a
defined way of saying "the sought element is not here," and it is also
the value `find` returns when it fails to find one. An invalidated
iterator, by contrast, is **unspecified**: its value is not even open to
comparison, and writing `it == other_it` can itself enter undefined
behavior, because the standard makes no guarantee at all about which
operations an invalidated iterator supports. This distinction matters
because checking a code's **empty** state (`it == end()`) is always safe,
but the question "is this iterator still valid" cannot be answered by
asking the iterator itself — validity is not information the iterator
carries, it is information that must be inferred from the operations the
source has gone through.

#### `push_back` and Reallocation: The Operation That Affects Every Iterator

`erase` only affects positions after the one it targets; `std::vector::push_back`
can leave much wider damage. If a `vector`'s allocated capacity is full,
`push_back` allocates a new, larger block of memory, moves every existing
element there, and frees the old block — in this case, **every** iterator,
pointer and reference taken before the insertion is invalidated, not only
the ones belonging to the inserted element. If capacity is sufficient, no
move happens at all, and existing iterators stay unchanged; but the
caller cannot know this in advance without comparing `size()` and
`capacity()`.

```cpp
// push-back-reallocation.cpp — how push_back affects iterators once capacity is full
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers;
    numbers.reserve(2);
    numbers.push_back(1);
    numbers.push_back(2);

    bool capacity_was_full = (numbers.size() == numbers.capacity());
    numbers.push_back(3);

    std::printf("was capacity full before the insertion %s\n",
        capacity_was_full ? "yes" : "no");
    std::printf("element count %zu\n", numbers.size());
    std::printf("first element %d\n", numbers.front());
    return 0;
}
```

```text
was capacity full before the insertion yes
element count 3
first element 1
```

**SL11.** The capacity reserved by `reserve(2)` was exactly full once two
elements had been inserted; the third `push_back` call therefore moved
the `vector` to a new block. Every iterator that might have been taken
before capacity was reached would have entered the **unspecified** state
once the move completed — this lesson does not hold onto such an iterator
and read it after the move, it only reads from the container itself again
through `front()`. `front()` returning `1` shows that the elements'
values were not affected by the move; what was affected was not the
elements' values but the validity of iterators taken to them beforehand.

#### Iterator Stability in `std::map`

Associative containers resemble `list` far more than `vector` on this
point. `std::map` is implemented as a balanced binary search tree, and
its elements do not sit in contiguous memory; when a node is inserted or
deleted, the other nodes do not change place, only the tree's links are
updated. This is why `std::map::erase`, like `std::list::erase`,
invalidates only the iterator to the deleted element; every remaining
iterator, pointer and reference stays valid. `std::map::insert` carries a
similar guarantee: inserting a new node never moves the existing nodes,
so no iterator taken before the insertion is invalidated.

```cpp
// map-erase-other-iterators.cpp — does map erase affect other iterators
#include <cstdio>
#include <map>

int main() {
    std::map<int, int> table{{1, 10}, {2, 20}, {3, 30}};
    auto three = table.find(3);

    table.erase(table.find(1));

    std::printf("value at the untouched key %d\n", three->second);
    std::printf("remaining element count %zu\n", table.size());
    return 0;
}
```

```text
value at the untouched key 30
remaining element count 2
```

**SL12.** The iterator belonging to `table.find(1)` was deleted, but
`three` pointed to a different key and was not affected by the `erase`
call — `three->second` is still `30`. This outcome's kind is the same as
its counterpart on `std::list`: **unchanged**. Had the same scenario been
set up on `std::vector` — deleting the first of three elements — the
iterators to the second and third elements would have been invalidated as
the remaining elements shifted back; the fact that they are not
invalidated here comes from `map`'s memory layout being fundamentally
different from `vector`'s, not from its erase operation being written
"more safely."

#### Why an Invalid Iterator Is Never Read

A line of code that wants to directly read the old iterator belonging to
a deleted element, instead of using the iterator `erase` returns,
compiles but **cannot be run** — this is undefined behavior that the
compiler cannot catch. The block below carries that line only as a
comment and explains why it is never run:

```cpp
// invalid-iterator-comment.cpp — the reasoning for never reading an invalid iterator
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{10, 20, 30, 40};
    auto it = numbers.begin() + 1;

    numbers.erase(it);

    // The line below is undefined behavior and is therefore never run:
    // std::printf("%d\n", *it);

    std::printf("element count after erase %zu\n", numbers.size());
    return 0;
}
```

```text
element count after erase 3
```

The compiler does not reject the line `*it`, because `it`'s validity is
not a type check, it is a runtime contract. Because the standard leaves
the behavior of code that violates this contract undefined, the only safe
path is to never write that line at all, or never run it — this is the
rule applied in every lesson of this course. This also explains why some
static analysis tools cannot fully catch this class of bug: an
iterator's validity does not live in the iterator's type, it lives in the
history of operations the container that produced it has gone through —
unless a tool tracks that history, it cannot see that a call that looks
valid is in fact reaching an invalidated position.

#### The Bounding Measurement

The same `erase` call is bounded by the iterator it targets — what
happens to the remaining iterators depends on the **container's type**,
not on the operation itself. Consider a four-element `std::list`: delete
the two middle elements and read the iterators at the ends again:

```cpp title="edge-iterators-remaining.cpp"
#include <cstdio>
#include <iterator>
#include <list>

int main() {
    std::list<int> numbers{1, 2, 3, 4};

    auto first = numbers.begin();
    auto last = std::prev(numbers.end());
    auto middle_one = std::next(numbers.begin());
    auto middle_two = std::next(numbers.begin(), 2);

    numbers.erase(middle_one);
    numbers.erase(middle_two);

    std::printf("%d %d\n", *first, *last);
    return 0;
}
```

```text title="output"
1 4
```

`erase(middle_one)` invalidates only the node `middle_one` points to;
`first` and `last` are bound to different nodes and are unaffected. The
second `erase` call follows the same rule: `middle_two` becomes invalid
along with its own node, but this adds nothing to `middle_one`, which was
already deleted — `first` and `last` keep showing their pre-operation
values **unchanged**. Had the same scenario been built with a
`std::vector`, the result would differ: when an element is deleted, every
element after it shifts back by one position, so an iterator taken from
the end, like `last`, would also be invalidated. What determines the
outcome's kind is not the number of deleted elements or where the
iterator was taken from, but the container's memory layout: in a
node-based structure only the touched node is invalidated, while a
deletion in contiguous memory affects every position after it.

#### Summary

- `std::vector::erase` invalidates the iterator to the deleted element;
  reading it after the erase is **unspecified** behavior and is never
  run. The new iterator the call returns is valid and points to the
  position after the removed element. Bounding measurement: the same
  operation on a `std::list` invalidates only the node it targets;
  untouched iterators stay **unchanged** — validity is determined not by
  the operation but by the container's memory layout.
- `std::list::erase` and `std::map::erase`/`insert`, in their node-based
  structures, invalidate only the iterator to the touched node; every
  other iterator stays **unchanged** — since memory is not contiguous, no
  element shifting ever happens.
- Iterators fall into input, forward, bidirectional and random access
  categories; the category determines which operations are supported.
- The kind of this lesson's central measurement is **unspecified**: the
  state of an iterator belonging to an erased element is not defined by
  the standard, and the only guarantee is that it must never be read
  again.
- `end()` is always a defined, comparable value; a container being
  **empty** is expressed by `begin() == end()`. This is separate from the
  **unspecified** state of an invalidated iterator — one says "there is
  no element here," the other carries no guarantee at all.
- `std::vector::push_back`, if capacity is full, invalidates not only the
  inserted element's iterator but every iterator taken beforehand; if
  capacity is sufficient, no iterator is affected. This is why it is
  unsafe to rely on previously taken iterators while calling `push_back`
  inside a loop over a `vector`.

#### Next Step

This lesson focused on the validity of a single iterator. The next lesson
moves on to standard algorithms that operate over all of a container's
elements: how does `std::sort` change a container in place, and which
iterators does that change affect?

### Algorithms

Source: https://academia.sh/en/courses/cpp-standard-library/algorithms

The measurement of how std::sort changes a vector in place, and how read-only algorithms such as std::count leave the source unchanged.

The previous lesson showed that reading an iterator after `erase` is
**unspecified**, while the new iterator `erase` returns stays valid. That
lesson tracked a single position. This lesson widens the boundary back
out: it turns to the standard library's ready-made algorithms that
operate on **all** of a container's elements. What does `std::sort` leave
in the source when it sorts a `vector`? This same question will be asked
again for every algorithm in this lesson; some algorithms read the source
and never change it, while others permanently reorder the source itself,
and this difference is drawn not from the algorithm's name but from its
standard definition.

#### `std::sort`: An Algorithm That Changes in Place

`std::sort` arranges the elements in the given iterator range according
to the specified ordering rule **in place** — it returns no new container,
it changes the given range itself. The default rule is `operator<`.

```cpp
// sort-in-place.cpp — how std::sort changes a vector
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{40, 10, 30, 20};

    std::sort(numbers.begin(), numbers.end());

    std::printf("sorted array:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
sorted array: 10 20 30 40
```

**SL13.** After the call to `std::sort`, `numbers`'s element count did not
change, but the elements' **order** changed — the kind of this operation
is **changed**. The standard guarantees exactly which final arrangement
results after sorting (ascending, by the `operator<` rule), so the
outcome is not unspecified, it is on the contrary predictable and
repeatable. This is what separates `sort` from `erase` in the previous
lesson: the iterator `erase` left behind was unspecified, but the array
`sort` leaves behind is defined.

#### Read-Only Algorithms: The Ones That Leave the Source Unchanged

Not every algorithm changes the source. `std::count` returns the number
of elements equal to the given value; `std::find_if` returns an iterator
to the first element that satisfies a predicate; `std::accumulate`
accumulates the elements by summing them. All three only read the range.

```cpp
// read-only-algorithms.cpp — do count, find_if, accumulate change the source
#include <algorithm>
#include <cstdio>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> numbers{4, 7, 2, 7, 9};
    auto before = numbers;

    long count = std::count(numbers.begin(), numbers.end(), 7);
    auto it = std::find_if(numbers.begin(), numbers.end(),
        [](int value) { return value > 8; });
    int total = std::accumulate(numbers.begin(), numbers.end(), 0);

    std::printf("count of 7 %ld\n", count);
    std::printf("first value greater than 8 %d\n", *it);
    std::printf("total %d\n", total);
    std::printf("did the source change %s\n", (numbers != before) ? "yes" : "no");
    return 0;
}
```

```text
count of 7 2
first value greater than 8 9
total 29
did the source change no
```

**SL14.** All three algorithms only read `numbers`; at the end of the
operation the source is equal to its pre-operation copy — the kind is
**unchanged**. This stands in direct contrast to `std::sort`: both
operate on the same `vector`, but `sort` changes the ordering while these
three algorithms never touch the ordering at all. An algorithm's name
usually indicates its intent — the verbs `count`, `find`, `accumulate`
say "query," the verbs `sort`, `reverse`, `transform` say "change" — but
this is a convention, not a guarantee; the definitive answer is always in
the algorithm's standard definition.

#### This Course Does Not Count: The Measurement Axis Is Not Run Count

`Traits, Generics and Error Handling` (Rust, M10/K03) made the runtime
cost of abstraction visible by measuring how many times a body **runs** —
what was measured there was how many times a call was repeated. This
course works on a different axis and **does not count**: a number such as
the `2` that `std::count` returns is a **result** that states how many
elements equal the sought value, not a measurement of how many times the
algorithm was called or how many times a body ran. The question this
lesson always asks is the same: which kind (unchanged/changed/empty/unspecified)
did an operation leave the source in? `std::count` itself is called once
and leaves the source **unchanged**; how many comparisons it performs
internally is not this measurement's subject. This distinction matters so
a reader does not mix up the two courses: a reader who wants to learn the
runtime cost of abstraction looks to `rust-soyutlama`; a reader who wants
to learn what state an operation leaves the source in looks to this
course.

#### A Custom Comparison Predicate

`std::sort` can take a third argument: a predicate that compares two
elements. This predicate replaces `operator<`, and the programmer defines
the ordering rule.

```cpp
// sort-predicate.cpp — descending order with a custom comparison
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{40, 10, 30, 20};

    std::sort(numbers.begin(), numbers.end(),
        [](int a, int b) { return a > b; });

    std::printf("descending:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
descending: 40 30 20 10
```

**SL15.** When the predicate returns `a > b`, it states that `a` comes
before `b`; this is why the array was sorted from largest to smallest.
This operation's kind is also **changed** — the same kind as the previous
`sort` call, because both are the same algorithm called with a different
rule. The predicate itself never read or changed `numbers`; it only
compared two `int` values. This distinction matters: what makes `sort`
mutating is that it writes into the iterator range, while what keeps the
predicate non-mutating is that it is only a value comparison.

#### Algorithms Work with an Iterator Pair, Not a Container

None of the standard algorithms take a container type; all of them take a
begin and end iterator. This lets the same algorithm work on all of a
`vector`, a sub-range of a `vector`, or a C array.

```cpp
// iterator-pair.cpp — the same algorithm over different ranges
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{5, 3, 8, 1, 9, 2};

    std::sort(numbers.begin(), numbers.begin() + 3);

    std::printf("partially sorted array:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
partially sorted array: 3 5 8 1 9 2
```

**SL16.** `std::sort(numbers.begin(), numbers.begin() + 3)` sorted only
the first three elements; the remaining three (`1 9 2`) stayed in their
input order. This shows that the same `std::sort` call produces a
different result with a different range — the algorithm sees not the
whole container but only the iterator pair it was given. This
operation's kind is again **changed**, because part of `numbers` (the
first three elements) changed in ordering; but this time the scope of the
change is limited to the given range, not the whole container. One
consequence of this design is that algorithms never know a container's
own bounds: the iterator `numbers.begin() + 3` given to `std::sort` does
not denote the container's own end, only the bound the caller specified.
The same algorithm works the same way on a raw C array too, called with a
pointer pair — the concept of a container never appears in the
algorithm's interface, only the iterator category (here, random access)
is what the interface requires.

#### `std::transform`: An Algorithm That Writes to a New Destination

`std::sort` changed the source range itself. `std::transform` offers a
different pattern: it reads the source, applies a function to each
element, and writes the result to a **separate** destination. As long as
the source is a container separate from the destination, `std::transform`
never touches the source.

```cpp
// transform-separate-destination.cpp — does std::transform change the source or the destination
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> source{1, 2, 3, 4};
    std::vector<int> destination(source.size());

    std::transform(source.begin(), source.end(), destination.begin(),
        [](int value) { return value * value; });

    std::printf("source:");
    for (int value : source) {
        std::printf(" %d", value);
    }
    std::printf("\n");

    std::printf("destination:");
    for (int value : destination) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
source: 1 2 3 4
destination: 1 4 9 16
```

**SL17.** `source` is still `1, 2, 3, 4` at the end of the operation —
`std::transform` only read the source and never wrote to it; the
source's kind is **unchanged**. `destination` was filled with the array
of squares. `std::transform`'s third argument is the destination's begin
iterator; this argument can also be given as the source itself
(`source.begin()`) — in that case the algorithm reads each element and
writes it back to the same position, and the source then falls into the
**changed** kind instead. Which kind applies depends on whether the
destination is the same range as the source; this lesson keeps the two
separate to show `std::transform`'s default behavior, which is to read
the source and write elsewhere. Because `std::transform`'s destination
argument is an iterator, it is the caller's responsibility to have
already allocated enough size at the destination beforehand — the
algorithm does not insert elements into the destination, it only writes
to existing positions; if the destination is not as large as `source`,
the write runs past the bound of allocated memory, and this is undefined
behavior. This lesson never creates this risk, by always sizing
`destination` with `source.size()`.

#### `std::stable_sort`: A Sort That Preserves the Order of Equal Elements

`std::sort` does **not guarantee** preserving the relative order between
two elements considered equal — this is a permitted side effect of the
algorithm, not a bug. `std::stable_sort` does the same sorting work but
carries an additional guarantee: elements considered equal by the
comparison predicate keep their relative position from the input order.
This guarantee is not free — `std::sort` runs with average `O(n log n)`
complexity, while `std::stable_sort` can drop to `O(n log^2 n)` if
sufficient extra memory is not available; if extra memory is available,
it still achieves `O(n log n)`.

```cpp
// stable-sort-equality.cpp — how the order of equal elements differs between sort and stable_sort
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>

int main() {
    std::vector<std::pair<int, char>> records{
        {2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};

    std::stable_sort(records.begin(), records.end(),
        [](const auto& left, const auto& right) { return left.first < right.first; });

    std::printf("sorted records:");
    for (const auto& record : records) {
        std::printf(" (%d,%c)", record.first, record.second);
    }
    std::printf("\n");
    return 0;
}
```

```text
sorted records: (1,b) (1,d) (2,a) (2,c)
```

**SL18.** The pair `{1, 'b'}` and `{1, 'd'}`, considered equal by key,
kept their relative order from the input after the sort — `b` still comes
before `d`. The same holds for `{2, 'a'}` and `{2, 'c'}`. This
operation's kind is also **changed** — the array was reordered from start
to end — but its difference from `std::sort` is the extra guarantee that
the relative order among equal elements was also preserved. `std::sort`
could have left `(1,d)` and `(1,b)` in reverse order on the same input;
this is not a rule `std::sort` violates, it is a promise it never makes.

#### Why `std::list` Carries Its Own `sort`

`std::sort` requires a random access iterator — it needs to jump between
elements in constant time in order to achieve its average `O(n log n)`
complexity. This is why `std::sort` **does not compile** with
`std::list`'s bidirectional iterator. The standard library provides a
separate member function for this case: `std::list::sort`.

```cpp
// list-sort-member-function.cpp — why list needs a separate sort
#include <cstdio>
#include <list>

int main() {
    std::list<int> values{40, 10, 30, 20};

    // The line below does not compile, because std::sort requires a random access iterator:
    // std::sort(values.begin(), values.end());

    values.sort();

    std::printf("sorted list:");
    for (int value : values) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
sorted list: 10 20 30 40
```

The reason `std::list::sort` exists as a separate member function is this
constraint: because `list`'s nodes do not sit in contiguous memory, the
generic `std::sort` algorithm cannot operate on them, but `list` can
produce the same result at a different complexity by relinking its own
nodes. This is a concrete example of the rule "not every container works
with every algorithm." The same constraint applies to other algorithms
that require random access: `std::nth_element` and binary-search-based
algorithms such as `std::binary_search` and `std::lower_bound` also
cannot be called directly on `std::list`; the standard library does not
fill this gap with a separate member function for every container, it
only offers a few operations that require node linkage, such as
`list::sort`, `list::merge`, `list::unique` and `list::remove`, as member
functions. The choice of container therefore determines not only the
complexity guarantees but also which standard algorithms can be used
directly.

#### The Bounding Measurement

When the same data is given to two different algorithms with a comparison
predicate over the same key, the question of how much of the source can
be reliably read depends on the algorithm's **name**. `std::sort` does
not guarantee preserving the relative order between elements with equal
keys — so printing that order would be presenting a promise the standard
never made. The same data is sorted once more below with `std::stable_sort`,
and the two results are compared:

```cpp title="what-a-name-tells-about-order.cpp"
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>

int main() {
    std::vector<std::pair<int, char>> keyed{
        {2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};
    std::vector<std::pair<int, char>> stable = keyed;

    std::sort(keyed.begin(), keyed.end(),
        [](const auto& left, const auto& right) { return left.first < right.first; });
    std::stable_sort(stable.begin(), stable.end(),
        [](const auto& left, const auto& right) { return left.first < right.first; });

    std::printf("sort key order:");
    for (const auto& record : keyed) {
        std::printf(" %d", record.first);
    }
    std::printf("\n");

    std::printf("stable_sort full order:");
    for (const auto& record : stable) {
        std::printf(" %d%c", record.first, record.second);
    }
    std::printf("\n");
    return 0;
}
```

```text title="output"
sort key order: 1 1 2 2
stable_sort full order: 1b 1d 2a 2c
```

Over `keyed` we printed only the keys (`1 1 2 2`); we did not print the
letters, because which letter `std::sort` leaves first among equal keys
is a detail the standard does not define — even the same compiler may
apply a different internal ordering at a different input size. Over
`stable`, we could print the full records, because `std::stable_sort`
guarantees preserving the relative input order of equal keys: `1b` always
comes before `1d`, `2a` always comes before `2c`. Both calls move the
same data into the **changed** kind — the source is reordered in place —
but which detail of the source can be safely read and reported is
determined by the algorithm's name: `sort` guarantees only the final key
order, `stable_sort` guarantees the key order together with the relative
position among equals.

#### Summary

- `std::sort` changes the order of the elements in the given iterator
  range in place; the kind of this lesson's central measurement is
  **changed**. Giving a custom comparison predicate changes the ordering
  rule, but the predicate itself never reads or changes the source;
  `std::stable_sort` also preserves the relative order of elements
  considered equal, and this extra guarantee comes at a complexity cost
  that can rise to `O(n log^2 n)` when sufficient extra memory is not
  available. Bounding measurement: when the same data is given to
  `std::sort` and `std::stable_sort`, both move the source into the
  **changed** kind, but which detail of the source can be safely reported
  depends on the algorithm's name — `sort` guarantees only the key order,
  `stable_sort` also guarantees the relative order among equals.
- Read-only algorithms such as `std::count`, `std::find_if`,
  `std::accumulate` leave the source **unchanged**.
- This course does not count runs: `rust-soyutlama`'s "how many times did
  it run" axis is not this course's measurement axis; what is measured
  here is which kind the source is left in.
- Algorithms take an iterator pair, not a container type; the same
  algorithm operates over a different scope with a different range.
- `std::transform` leaves the source's kind **unchanged** when it reads
  the source and writes to a separate destination; making the destination
  the source itself turns the kind into **changed**.
- `std::sort` requires a random access iterator and does not compile on
  `std::list`; `list` instead provides its own `sort` member function.

#### Next Step

This lesson showed how `std::sort` changes a container in place. The next
lesson takes up the two-step idiom required to **remove** elements from a
container: why does `std::remove` not actually delete elements, only move
them toward the end?

### The Erase–Remove Idiom

Source: https://academia.sh/en/courses/cpp-standard-library/erase-remove-idiom

The measurement of how the elements left in a vector's tail after std::remove carry an unspecified value, and how erase actually removes that tail.

The previous lesson showed that `std::sort` reorders a `vector`'s
elements in place, which **changes** the source. The container's element
count never changed during the sort. This lesson moves to an operation
that **reduces** the element count, but with an unexpected detail:
`std::remove`, whose name includes "remove," does not change a `vector`'s
size. So what does it do?

This detail is one of the points that newcomers find most surprising: a
function's name never lines up exactly one to one with what it actually
does. `std::remove` is an algorithm, and algorithms only work with
iterator pairs — they have no access to the container itself, and
therefore no authority to shrink the container's size either. This lesson
shows step by step where this boundary comes from and how the actual
removal is completed.

#### `std::remove`: A "Removal" That Does Not Remove

`std::remove` walks the given range; it moves elements not equal to the
sought value toward the front of the range and returns an iterator
marking where the new "valid" region ends. It does not change the
container's size — it cannot, because it works with an iterator pair, not
with the container itself.

```cpp
// remove-without-erase.cpp — what std::remove does on its own
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{1, 7, 2, 7, 3, 7};
    std::size_t before = numbers.size();

    auto new_end = std::remove(numbers.begin(), numbers.end(), 7);

    std::printf("did the size change %s\n",
        (numbers.size() != before) ? "yes" : "no");
    std::printf("length of the valid region %ld\n",
        static_cast<long>(std::distance(numbers.begin(), new_end)));
    return 0;
}
```

```text
did the size change no
length of the valid region 3
```

**SL19.** After the call to `std::remove`, `numbers.size()` is still `6`
— the container's size did not change. The `new_end` iterator shows that
the valid region is three elements long: the values `1, 2, 3` were moved
forward. The remaining three positions (from `new_end` to `numbers.end()`)
still exist, but their contents are no longer meaningful. The kind of
this first step is **changed**: the array's first three positions carry a
defined result (`1, 2, 3`).

#### The Values Left in the Tail: Unspecified

The positions from `new_end` to `numbers.end()` depend on `std::remove`'s
implementation detail — the standard does not define what these
positions' values will be, it only says they will carry "an unspecified
value." This lesson does not **read** those positions.

```cpp
// remove-does-not-read-the-tail.cpp — why the region outside the valid one is never looked at
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{1, 7, 2, 7, 3, 7};

    auto new_end = std::remove(numbers.begin(), numbers.end(), 7);

    std::printf("valid region:");
    for (auto it = numbers.begin(); it != new_end; ++it) {
        std::printf(" %d", *it);
    }
    std::printf("\n");
    // The values from new_end to numbers.end() are not read,
    // because the standard does not define the values in this region.
    return 0;
}
```

```text
valid region: 1 2 3
```

**SL20.** This lesson never read the three tail positions (after
`new_end`) in any `printf` call — not to avoid undefined behavior
(reading the tail is not undefined behavior, the elements still exist and
are valid), but to avoid presenting an **unspecified** value as if it
were a definite number. Because the standard does not define what values
these positions will carry, printing these values could produce a
different output in a different standard library implementation five
years from now. The kind of this operation is **unspecified**: the
content of the tail positions is not guaranteed by the standard.

#### Actually Removing the Tail with `erase`

Giving the iterator `std::remove` returns to the container's own `erase`
member function actually removes the tail and shrinks the container's
size. This two-step pattern is called the **erase–remove idiom**.

```cpp
// remove-erase-idiom.cpp — actual removal in two steps
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{1, 7, 2, 7, 3, 7};

    auto new_end = std::remove(numbers.begin(), numbers.end(), 7);
    numbers.erase(new_end, numbers.end());

    std::printf("size %zu\n", numbers.size());
    std::printf("contents:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
size 3
contents: 1 2 3
```

**SL21.** After the call `erase(new_end, numbers.end())`,
`numbers.size()` dropped to `3` and the contents are only `1, 2, 3`. The
kind of this step is **changed**: both the size and the contents reached
a definite result, in the form the standard defines. Combining the two
steps gives this picture: `remove` alone puts the source into the
**changed** state but leaves the tail **unspecified**; once `erase` is
added, the tail is also removed and the source becomes fully **changed**
again — no unread unspecified region remains. This is also where the
reason for keeping the two steps separate becomes clear: because
`remove` only works with an iterator pair, it cannot shrink the container
itself; the authority to shrink the container exists only in that
container's own `erase` member function. The two separate steps reflect
two separate responsibilities: one takes on moving the elements, the
other takes on giving back the memory.

#### C++20: The `std::erase` Free Function

C++20 added free functions that collapse this frequently used two-step
pattern into a single call: `std::erase` and `std::erase_if`. These
functions are defined in the `<vector>` header and apply the
`remove`+`erase` pair without requiring a container-specific member
function.

```cpp
// std-erase-free-function.cpp — erase-remove in one call
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{1, 7, 2, 7, 3, 7};

    std::erase(numbers, 7);

    std::printf("size %zu\n", numbers.size());
    std::printf("contents:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
size 3
contents: 1 2 3
```

**SL22.** `std::erase(numbers, 7)` did in one line what the previous two
steps (SL19 and SL21) did; the result is the same: size `3`, contents
`1, 2, 3`. The kind of this operation is also **changed** — this time no
unspecified intermediate state was ever visited, because the tail move
and the actual removal happened inside a single function, without ever
exposing an intermediate iterator to the caller. The older `remove`+`erase`
idiom still compiles and is still seen in pre-C++20 code; `std::erase`
gives the same result with fewer lines of code and hides the intermediate
unspecified state.

#### This Course Does Not Count: The Measurement Axis Is Not How Many Elements Were Deleted

The axis of the "Traits, Generics and Error Handling" lesson (the
`rust-soyutlama` course in the Rust curriculum) is how many times a body
**runs** — it measures how many times a predicate or a closure is called.
This lesson stands on a different axis: how many elements
`std::erase_if` removes is not this lesson's subject. `numbers.size()`
dropping from `6` to `3` is not a **counter**, it is a single value
showing the container's current state — just as, in the previous lesson,
the number `std::count` returned was a result, not a count of how many
times it was called. What this lesson measures is not the *number* of
removed elements, but which **kind** (unspecified or changed) the source
falls into after removal.

#### `std::remove`'s Complexity: Independent of the Number of Matches

`std::remove` and `std::remove_if` scan the range from start to end in a
**single pass**; their complexity is O(n), and this is independent of how
many elements in the range turn out equal to the sought value. The
algorithm never visits any element twice: it either moves the element
forward or skips over it, then advances to the next position.

```cpp
// remove-complexity.cpp — single pass, independent of the number of matches
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> none_match{1, 2, 3, 4, 5};
    std::vector<int> all_match{9, 9, 9, 9, 9};

    auto end1 = std::remove(none_match.begin(),
        none_match.end(), 9);
    auto end2 = std::remove(all_match.begin(),
        all_match.end(), 9);

    std::printf("none_match valid length %ld\n",
        static_cast<long>(std::distance(none_match.begin(), end1)));
    std::printf("all_match valid length %ld\n",
        static_cast<long>(std::distance(all_match.begin(), end2)));
    return 0;
}
```

```text
none_match valid length 5
all_match valid length 0
```

**SL23.** Both calls scan a range of the same size (5 elements) in a
single pass; in the first, no element matches, so the valid region
covers the whole range (`5`); in the second, every element matches, so
the valid region becomes empty (`0`). Both run in the same order of
magnitude of time — `std::remove` does not take a different path
depending on how many matches it finds, it visits every element exactly
once. This is an example showing that the algorithm's complexity depends
on the input's *length*, not its *content*.

#### `std::list::remove`: A Member Function That Needs No Idiom

While the `remove`+`erase` idiom is necessary for `std::vector`,
`std::list` provides its own `remove` and `remove_if` member functions,
and these need no idiom — the moment they are called, they actually take
matching nodes out of the linked list, no separate `erase` call is
needed.

```cpp
// list-remove-member-function.cpp — list provides its own remove
#include <cstdio>
#include <list>

int main() {
    std::list<int> numbers{1, 7, 2, 7, 3, 7};

    numbers.remove(7);

    std::printf("size %zu\n", numbers.size());
    std::printf("contents:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
size 3
contents: 1 2 3
```

**SL24.** `numbers.remove(7)` alone, without a separate `erase` call,
reached the same result the two-step idiom reached in the `vector`
example: size `3`, contents `1, 2, 3`. The kind of this operation is
**changed**. The difference is in the mechanism: because `std::list`
keeps its nodes in a linked structure, removing a node does not require
shifting neighboring nodes; the `remove` member function can perform this
removal directly. `std::vector`, because it keeps elements in contiguous
memory, gives the generic `std::algorithm` no authority to change the
container's size — this is why `std::remove` only moves elements, and the
actual removal is left to the container-specific `erase`.

#### `std::remove_if` and `std::erase_if`: Removal by a Predicate

`std::remove` only measures equality to a value. If the removal decision
depends on a condition, `std::remove_if` is used; its first argument is
not a value but a predicate. The idiom stays the same: the iterator
`remove_if` returns is given to `erase`, or `std::erase_if` is used in
C++20 for a single call.

```cpp
// remove-if-erase-if.cpp — removal by predicate
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{4, 15, 8, 23, 6, 42};

    std::erase_if(numbers, [](int value) { return value > 10; });

    std::printf("size %zu\n", numbers.size());
    std::printf("contents:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
size 3
contents: 4 8 6
```

**SL25.** `std::erase_if` removed the three elements (`15, 23, 42`) that
satisfy the predicate (greater than `10`); the remaining three elements
kept their input order (`4, 8, 6`). The kind of this operation is
**changed**: the size dropped from `6` to `3`, and the standard defines
which elements remain (those that do not satisfy the predicate) and that
their relative order is preserved. The relationship between the
`remove_if`+`erase` pair and `erase_if` is the same as the relationship
between `remove`+`erase` and `erase`: both apply the same two steps,
`erase_if` only hides those steps in a single call and never exposes the
intermediate unspecified tail to the caller.

#### If the Tail Is Read: Not a Crash, an Undefined Value

Code that tries to loop over and print the positions from the `new_end`
iterator `std::remove` returns to `numbers.end()` **compiles** and
**runs** — there is no compile error or crash here. The problem is not
visible at runtime either: the program prints some number. The problem is
that which value that number will be is undefined across different
standard library implementations.

```cpp
// if-the-tail-is-read.cpp — why reading the tail is wrong, not a crash
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{1, 7, 2, 7, 3, 7};

    auto new_end = std::remove(numbers.begin(), numbers.end(), 7);

    std::printf("valid region length %ld\n",
        static_cast<long>(std::distance(numbers.begin(), new_end)));
    // The values in the tail are deliberately not printed: the standard
    // does not define them, so this lesson produces no output based on them.
    return 0;
}
```

```text
valid region length 3
```

This code **runs**, but a version of it that tries to read the tail has
its output never shown in this lesson, because that output would rest on
a value the standard does not guarantee. Run five years from now with a
different compiler or a different library implementation, the same line
could print a different number — this is exactly the sort of evidence
this lesson has avoided from the start.

C++20's `std::erase`/`std::erase_if` free functions are overloaded not
only for `std::vector` but also for `std::list`, `std::deque` and
`std::forward_list`. This provides a common interface independent of
container type: whichever container is being used, the operation "remove
the elements matching this value/predicate" is expressed with the same
call. But different mechanisms still run underneath this common
interface — for `std::vector` the `remove`+`erase` idiom is applied
internally, while for `std::list` direct node removal is done. A common
interface does not mean the implementation detail is the same; the
distinction followed throughout this lesson (the source's kind:
unchanged, changed, empty, unspecified) still applies to both mechanisms,
because the outcome is evaluated by looking at the source's state, not at
the implementation detail.

#### The Bounding Measurement

The `remove`+`erase` idiom makes the two steps visible separately and
briefly exposes the unspecified tail in between. `std::erase`, which
does the same work in one call, never shows this intermediate state, and
it even reports how many elements it removed — its return value is not a
predicate call counter, it is the operation's result:

```cpp title="erase-returned-count.cpp"
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{7, 1, 7, 2, 7};

    auto removed = std::erase(numbers, 7);

    std::printf("%zu removed, %zu remaining\n", removed, numbers.size());
    return 0;
}
```

```text title="output"
3 removed, 2 remaining
```

`std::erase(numbers, 7)` removed the three `7`s from the five-element
`numbers` and reported this with a single return value: `removed` is `3`,
the remaining size is `2`. In the `if-the-tail-is-read.cpp` example
above, the tail was never read because its content was unspecified; here
there is **no tail at all** to read — because `std::erase` completes the
removal and size-shrinking steps internally, the source moves directly
into the **changed** kind, never visiting an intermediate unspecified
state. This shares the same root as what was seen above for the
`std::list::remove` member function: both fold the idiom's two steps
(move, then shorten) into a single operation without exposing them to the
caller; their only difference is in the mechanism — `std::list::remove`
changes node linkage, while `std::erase` on a `vector` internally calls
the `remove`+`erase` idiom itself. The difference between the
identically named `std::remove` (which does not remove) and
`std::list::remove` (which actually removes) reappears here too: both
carry the name "remove," but only the member function carries the
authority to actually remove.

#### Summary

- `std::remove` does not delete elements; it moves the ones not equal to
  the sought value forward and does not change the container's size. The
  tail region from the returned iterator to the container's end carries
  an **unspecified** value, and this lesson never reads that region.
- `erase(new_end, end())` actually removes the tail and shrinks the
  container's size; the kind of this step is **changed**.
- C++20's `std::erase` free function collapses the remove+erase idiom
  into a single line and delivers the same **changed** result without
  ever visiting an intermediate unspecified state; it also reports how
  many elements it removed through its return value. The bounding
  measurement confirms exactly this: the call `std::erase(numbers, 7)`
  moves the source directly into the **changed** kind without ever
  visiting the tail.
- The kind of this lesson's central measurement is **unspecified**: the
  content of the values in the tail after `std::remove` is not defined by
  the standard.
- The complexity of `std::remove`/`std::remove_if` is O(n) and is
  independent of how many elements in the range match; this lesson
  measures not the number of matches but the source's kind.
- `std::list` performs direct removal, needing no idiom, through its
  `remove`/`remove_if` member functions; why `std::vector`'s generic
  algorithm lacks this authority comes from the difference in the
  containers' memory layout.

#### Next Step

This lesson used the iterator an algorithm returns, but the comparison
was not always equality to a value — some calls passed a predicate. Up to
now these predicates could be thought of as an ordinary function pointer
or a named struct, but in practice they are mostly written as a small,
single-use expression. The next lesson moves to the mechanism behind
these predicates, lambda expressions and function objects: what happens
to a value when a lambda **captures** it?

### Lambdas and Function Objects

Source: https://academia.sh/en/courses/cpp-standard-library/lambdas-and-function-objects

The measurement of how a lambda's non-mutable capture-by-value leaves the source unchanged, and how the captured copy stays independent across calls.

The previous lesson searched with `std::remove` by passing it a value; but
some algorithms ask for a **predicate** instead of a value — something
callable that evaluates a condition. This lesson moves to the most common
form of that callable thing, lambda expressions. When a lambda
**captures** an outer variable — by writing `[value]` inside the square
brackets — what happens to that outer variable?

#### Capture by Value: Copying, Not Binding

A lambda that writes `[value]` embeds a **copy** of the `value` variable
inside the lambda object. This is not holding a reference to the outer
`value`; a snapshot of the value is taken at the moment the lambda is
created.

```cpp
// capture-by-value.cpp — does the lambda change the outer variable
#include <cstdio>

int main() {
    int value = 10;
    auto show = [value]() { std::printf("captured value %d\n", value); };

    value = 99;
    show();

    std::printf("outer value %d\n", value);
    return 0;
}
```

```text
captured value 10
outer value 99
```

**SL26.** When the lambda was created, `value` was `10`, and the lambda
embedded a copy of that `10`. Before the lambda was called, the outer
`value` was assigned `99`, but when `show()` was called it still printed
`10` — because the copy inside the lambda is completely independent from
the outer variable. The outer `value` itself also stayed **unchanged**:
the act of creating the lambda never modified the variable it captured;
the line `value = 99` was already a separate assignment operation, not
the "capture" operation this lesson measures.

#### `mutable`: Making the Captured Copy Modifiable

By default, a copy captured by value is treated as `const` inside the
lambda's call operator — it cannot be modified. The `mutable` keyword
removes this restriction and allows the captured copy to be modified
inside the lambda's body. This change affects only the copy, not the
outer variable.

```cpp
// mutable-capture.cpp — modifying the copy with mutable
#include <cstdio>

int main() {
    int step = 0;
    auto advance = [step]() mutable {
        step += 1;
        std::printf("step inside lambda %d\n", step);
    };

    advance();

    std::printf("outer step %d\n", step);
    return 0;
}
```

```text
step inside lambda 1
outer step 0
```

**SL27.** Thanks to `mutable`, the copy of `step` inside the lambda
changed from `0` to `1` during the `advance()` call — the lambda object
was able to make this change on its own copy. But the outer `step`
remained `0` after the call as well: **unchanged**. This clarifies what
`mutable` changes — not the outer variable, only the copy the lambda
embeds inside itself. This is where this course's central measurement
finds its answer for this lesson: capture by value, no matter what
`mutable` is written, never changes the outer source.

#### Capture by Reference: Direct Access to the Outer Variable

Writing `[&value]` captures a reference to the outer variable instead of
taking a copy. In this case, a change inside the lambda's body also
changes the outer variable — because there is no longer a separate copy.

```cpp
// capture-by-reference.cpp — capture by reference changes the outside
#include <cstdio>

int main() {
    int step = 0;
    auto advance = [&step]() { step += 1; };

    advance();

    std::printf("outer step %d\n", step);
    return 0;
}
```

```text
outer step 1
```

**SL28.** This time there was no need to write `mutable`, because what
was being modified was not a copy but the outer `step` itself. After the
`advance()` call, the outer `step` went from `0` to `1`: the kind of this
operation is **changed**. This stands directly opposite the
capture-by-value example at the start of the lesson: `[value]` left the
source unchanged while `[&step]` changes it. The difference is a single
character in the capture list (`&`), but the result is the exact
opposite.

#### Bulk Capture: `[=]`, `[&]`, and Mixed Lists

Instead of writing every outer variable one by one, `[=]` captures all
outer variables by value, and `[&]` captures all of them by reference.
The two can also be mixed: `[=, &value]` captures everything else by
value and only `value` by reference; `[&, value]` does the exact
opposite. Bulk capture leaves the choice of which variable to capture to
the compiler — every outer name used in the lambda's body is
automatically added to the list.

```cpp
// bulk-capture.cpp — mixed capture list
#include <cstdio>

int main() {
    int threshold = 10;
    int step = 0;

    auto evaluate = [=, &step](int value) {
        if (value > threshold) {
            step += 1;
        }
    };

    evaluate(15);

    std::printf("outer step %d\n", step);
    std::printf("outer threshold %d\n", threshold);
    return 0;
}
```

```text
outer step 1
outer threshold 10
```

**SL29.** In the capture list `[=, &step]`, `threshold` was captured by
value (as a copy), and `step` was captured by reference. The call
`evaluate(15)` read the copy of `threshold` but did not change it — the
outer `threshold` stayed **unchanged**. The same call **changed**
`step` through the reference: the outer `step` went from `0` to `1`. A
mixed capture list makes it possible for some variables in the same
lambda to stay unchanged while others change at the same time — the kind
decision is made separately for each name in the capture list; there is
no single kind for the list as a whole.

#### A Lambda Is Actually a Function Object

When the compiler sees a lambda expression, it generates an unnamed class
behind the scenes: this class has an `operator()` member function, and
the captured variables are that class's data members. A `[value]`
capture adds a member of `value`'s type to the generated class and
copies that member in the constructor. This explains why lambdas can be
used in the same places as hand-written function objects (ordinary
classes that define an `operator()`).

```cpp
// function-object-equivalent.cpp — a lambda and a hand-written class do the same job
#include <cstdio>

struct ValueDisplayer {
    int value;
    void operator()() const { std::printf("value inside class %d\n", value); }
};

int main() {
    int value = 42;

    ValueDisplayer class_displayer{value};
    auto lambda_displayer = [value]() { std::printf("value inside lambda %d\n", value); };

    class_displayer();
    lambda_displayer();
    return 0;
}
```

```text
value inside class 42
value inside lambda 42
```

**SL30.** The `ValueDisplayer` class and the `lambda_displayer` lambda
produced the same result, because both apply the same pattern: carrying
a value as a member and reading it inside `operator()`. Neither of these
calls changed `value`; both leave it **unchanged**, because both took
their own copy in the constructor and only read it. This equivalence
shows that lambdas are not a "special" language feature, but an ordinary
class the compiler writes automatically.

#### What the Capture List Leaves in the Source

The "Systems Programming with C" course (`c-systems-programming`) asked
whether a change **reaches** the other side: the writer's own copy,
parties sharing the same object, or no one at all. The capture list
immediately brings that question to mind — a lambda capturing with
`[&value]` "sees" the outer change, one capturing with `[value]` does
not. This lesson does not ask that question. What is measured here is
what is left **in the source itself** after the lambda is called: the
source is **unchanged** with capture by value, **changed** with capture
by reference, and **unspecified** when it was moved through an rvalue
capture. Seeing names a relationship; what is left behind names a state.
This course's measure is the state.

#### `std::function`: Keeping Different Callables in the Same Type

Every lambda has its own unique, unnamed type generated by the compiler —
two different lambda expressions are different types even if they carry
the same signature. `std::function<Result(Arguments...)>` is a wrapper
that lets these different types be kept in a single common container.
The act of wrapping does not change the callable being wrapped; it only
carries a copy of it.

```cpp
// std-function-wrapping.cpp — does std::function change the lambda it wraps
#include <cstdio>
#include <functional>

int main() {
    int multiplier = 3;
    auto lambda = [multiplier](int value) { return value * multiplier; };

    std::function<int(int)> wrapped = lambda;
    multiplier = 100;

    std::printf("wrapped(5) result %d\n", wrapped(5));
    std::printf("outer multiplier %d\n", multiplier);
    return 0;
}
```

```text
wrapped(5) result 15
outer multiplier 100
```

**SL31.** The line `std::function<int(int)> wrapped = lambda;` took a
copy of `lambda`'s state at that moment into `wrapped` — this is a
copying operation, just like the `[multiplier]` capture itself.
`multiplier` was later assigned `100`, but `wrapped(5)` still gave `15`
(`5 * 3`), because the copy inside `wrapped` carried `multiplier`'s
value from before the assignment. `lambda` itself came out of this
wrapping operation **unchanged**: assignment to `std::function` never
wrote to the `lambda` object, it only copied it. This is the same
pattern as the capture-by-value example at the start of the lesson — the
difference is that this time what is copied is not an `int`, but a
lambda object itself.

This copying carries a cost: to be able to hold callables of different
types in a single common type, `std::function` applies **type erasure**
— it hides the real type of the callable it wraps behind a base class.
This hiding usually requires a dynamic memory allocation and adds an
indirect jump (a mechanism similar to a virtual call) on every call. A
lambda assigned directly to an `auto` variable keeps its own concrete
type; the compiler can inline the call directly. `std::function` is
therefore not a tool that stands in for `auto` everywhere — it is
preferred when callables of different types need to be carried in the
same container (for example, keeping a list of callbacks in a
container), not just to hold a single lambda.

#### Generic Lambdas: `auto` as the Parameter Type

Starting with C++14, a lambda's parameter list can write `auto` instead
of a concrete type. This turns the lambda's call operator into a
template: the same lambda, when called with different types, generates a
separate `operator()` instance for each type. This has nothing to do
with the capture list — the kind (unchanged/changed) of the captured
variables is still determined by the same rules; only the call
parameter is generic.

```cpp
// generic-lambda.cpp — an auto-parameter lambda works with different types
#include <cstdio>

int main() {
    auto show = [](auto value) {
        std::printf("show called with a value\n");
        (void)value;
    };

    show(7);
    show(3.14);

    return 0;
}
```

```text
show called with a value
show called with a value
```

**SL32.** The `show` lambda was called with both an `int` and a
`double`; the compiler generated a separate `operator()` instance for
each call, but there is a single lambda expression in the source code.
This is the same mechanism as the general behavior of template
functions — an `auto` parameter turns the lambda's call operator into a
template member function. This has no direct bearing on this lesson's
central axis (the kind of the captured variable); a generic lambda only
makes the type of the *call parameter* flexible, it does not change the
capture list's behavior.

#### If `mutable` Is Not Written

Inside the body of a lambda without `mutable`, a line that tries to
assign directly to a variable captured by value **does not compile** —
because `operator()` is generated as `const` unless `mutable` is
written, and a `const` member function cannot assign to data members.

```cpp
// const-lambda-body.cpp — why assignment fails to compile without mutable
#include <cstdio>

int main() {
    int value = 0;
    auto read = [value]() { std::printf("value %d\n", value); };

    // The line below does not compile, because operator() is const
    // and value cannot be modified:
    // auto broken = [value]() { value += 1; };

    read();
    return 0;
}
```

```text
value 0
```

This restriction is not a shortcoming, it is a guarantee: unless
`mutable` is written, it is enforced at compile time that a lambda will
read the same captured value the same way on every call, without
silently accumulating state across calls. If a lambda is meant to hold
state across calls, that intent must be stated explicitly by writing
`mutable`.

This guarantee also determines which question a reader must ask when
examining a lambda: is there an `&` in the capture list or not, and if
so, is `mutable` written. The answer to these two questions alone
determines, for every variable the lambda captures, whether the source
will remain **unchanged** or become **changed** — no runtime observation
is needed either.

#### The Bounding Measurement

All three lambdas carry the same body — increment the captured name once
and print it — but the moment only the capture form changes, the kind of
the outer source splits into three:

```cpp title="three-captures-three-kinds.cpp"
#include <cstdio>
#include <string>
#include <utility>

int main() {
    int step = 0;
    auto by_value = [step]() mutable {
        ++step;
        std::printf("inside body: %d\n", step);
    };
    by_value();

    int position = 0;
    auto by_reference = [&position]() mutable {
        ++position;
        std::printf("inside body: %d\n", position);
    };
    by_reference();

    std::string s = "data";
    auto by_move = [t = std::move(s)]() {
        std::printf("inside body: %s\n", t.c_str());
    };
    by_move();
    s = "new-value";

    std::printf("outer step: %d\n", step);
    std::printf("outer position: %d\n", position);
    std::printf("outer s: %s\n", s.c_str());
    return 0;
}
```

```text title="output"
inside body: 1
inside body: 1
inside body: data
outer step: 0
outer position: 1
outer s: new-value
```

All three captures work as expected inside the body, but the trace they
leave on the outer source falls into three different kinds. `[step]`
captures by value; even though `mutable` allows the copy to be
incremented, this increment affects only the lambda's own copy, and the
outer `step` stays **unchanged** at `0`. `[&position]` captures by
reference; `++position` inside the body directly changes the outer
variable, and the outer `position` **changed** to `1`. `[t =
std::move(s)]` is an rvalue capture: the resource `s` held is moved into
`t`, and after the move, `s` is left in a valid but **unspecified**
state — this is why this lesson never reads or prints `s`'s content or
length after the move. What makes `s` usable again is assigning it a new
value: after the line `s = "new-value"`, `s` has defined content again,
and it is safe to read it — the last line of output shows this. The same
body, with three different capture forms, leaves the source in three
different kinds: unchanged, changed, unspecified.

#### Summary

- Capture by value with `[value]` takes a copy of the outer variable; the
  outer variable comes out of the lambda-creation operation
  **unchanged**.
- `mutable` makes the captured copy modifiable inside the lambda's body,
  but this change affects only the copy, not the outer source.
- Capture by reference with `[&value]` accesses the outer variable
  itself; in this case a change inside the lambda's body also
  **changes** the outside.
- A lambda is an instance of an unnamed class the compiler generates; the
  captured variables are that class's data members, and the lambda's
  body is its `operator()`.
- The kind of this lesson's central measurement is **unchanged**: with
  or without `mutable`, capture by value never changes the outer source.
- The bounding measurement: the same body produces three different
  kinds with three different captures — capture by value is
  **unchanged**, capture by reference is **changed**, and rvalue
  (move) capture leaves the source **unspecified**, and this lesson
  never reads that source before reassigning it.

#### Next Step

This lesson showed how a lambda carries the value it captures inside
itself, when `mutable` makes that copy modifiable, and why capture by
reference directly affects the outer source. The next lesson takes up a
way to view a container's elements without transforming them and without
building a new container: how does a range view, as a window looking
into the underlying container, leave the source unchanged?

### Ranges

Source: https://academia.sh/en/courses/cpp-standard-library/ranges

The measurement of how a view constructed from an lvalue leaves the underlying container unchanged, and how a view chain is evaluated lazily.

The previous lesson showed that a lambda capturing by value embeds a
copy of the variable inside itself and leaves the outer source
**unchanged**. This lesson moves to C++20's ranges library
(`std::ranges`) and asks a similar question: when a **view** is
constructed from an underlying container, does it copy that container,
or does it become only a window looking into it?

#### A View Does Not Copy the Container

`std::views::filter` produces a view that shows only the elements of a
given range that satisfy a condition. This view does not hold its own
elements; every time it is traversed it looks at the underlying range.

```cpp
// view-does-not-copy.cpp — does filter copy the underlying vector
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers{1, 2, 3, 4, 5, 6};

    auto evens = numbers | std::views::filter(
        [](int value) { return value % 2 == 0; });

    numbers.push_back(8);

    std::printf("elements in view:");
    for (int value : evens) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    std::printf("source vector size %zu\n", numbers.size());
    return 0;
}
```

```text
elements in view: 2 4 6 8
source vector size 7
```

**SL33.** `8` was added to `numbers` **after** the `evens` view was
created, and when the view was traversed this new element also entered
the list (`8` is even, so it passed the filter). This proves that
`evens` did not take a copy of `numbers` — if it had taken a copy, the
change made after `push_back` would not appear in the view. `numbers`
itself came out of this view-construction operation **unchanged**: the
`views::filter` call never wrote to the source, it only built a
definition that looks at it. `push_back` was a separate operation and is
not the "view construction" step this lesson measures.

#### A View Chain Is Evaluated Lazily

Multiple views can be chained with the `|` operator. `std::views::transform`
produces a view that applies a function to every element. When a chain
is built, no element is processed immediately — the processing happens
only while the chain is traversed, element by element.

```cpp
// lazy-view-chain.cpp — when do filter and transform actually run
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers{1, 2, 3, 4, 5, 6};

    auto chain = numbers
        | std::views::filter([](int value) { return value % 2 == 0; })
        | std::views::transform([](int value) {
              std::printf("transforming: %d\n", value);
              return value * 10;
          });

    std::printf("chain built\n");

    for (int value : chain) {
        std::printf("produced value %d\n", value);
    }
    return 0;
}
```

```text
chain built
transforming: 2
produced value 20
transforming: 4
produced value 40
transforming: 6
produced value 60
```

**SL34.** When the line "chain built" was printed, the `printf` inside
`transform` had not run at all — the `filter` and `transform` calls had
not processed a single element up to that point, they had only built a
definition. The "transforming" line first appeared exactly when the
`for` loop began traversing the chain, at the point when the first
element's turn came, and each "transforming" line appeared right before
its own "produced value" line, one at a time — one element was never
moved to the next before being processed and printed. This order of
output (first "chain built," then element-by-element interleaved
"transforming"/"produced value" pairs) shows when the chain actually
runs: not at construction time, but during traversal, element by
element. The source `numbers` did not change at any step of this chain
— **unchanged**.

#### `filter` and `transform`: A Pipeline

Chaining two views builds a pipeline: `filter` decides first which
elements will pass, `transform` is applied only to the elements that
passed. This order matters — if the order changes, the result the chain
produces can also change.

```cpp
// filter-transform-pipeline.cpp — how order affects the produced result
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers{1, 2, 3, 4, 5};

    auto filter_then_transform = numbers
        | std::views::filter([](int value) { return value % 2 == 0; })
        | std::views::transform([](int value) { return value * value; });

    std::printf("filter then transform:");
    for (int value : filter_then_transform) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
filter then transform: 4 16
```

**SL35.** The chain first selected the even numbers (`2, 4`), then
squared them (`4, 16`). `numbers` itself never changed throughout this
pipeline — `numbers` is still `{1, 2, 3, 4, 5}`; **unchanged**. The name
`filter_then_transform` states explicitly the order in which the chain
should be read (left to right, `filter` first); this is a habit that
prevents confusion when reading range chains — as a chain grows longer,
which step runs first should be read from the chain's own order, not
from the name.

#### What Constructing a View Leaves in the Source

The "Systems Programming with C" course (`c-systems-programming`) asked
whether a change **reaches** the other side. Views immediately bring
that question to mind: a view does not hold its own elements, it reads
from the underlying container every time it is traversed. But this
lesson does not use the question "does the view see a change made
afterward" as its measure. What is measured here is what
**constructing** the view leaves in the source: `views::filter`,
`views::transform`, and `views::take` constructed from an lvalue
container do not write to the source, the source stays **unchanged**.
As we will see in the bounding measurement, the answer is different for
a view constructed from an rvalue.

#### `views::take`: The First N Elements of a Range

`std::views::take` produces a view that shows only the first `n`
elements of a given range. Like other views, this one also does not
copy the underlying container; it only builds a bound that stops
traversal after `n` elements.

```cpp
// views-take.cpp — does take copy the underlying container
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers{10, 20, 30, 40, 50};

    auto first_three = numbers | std::views::take(3);

    std::printf("first three:");
    for (int value : first_three) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    std::printf("source vector size %zu\n", numbers.size());
    return 0;
}
```

```text
first three: 10 20 30
source vector size 5
```

**SL36.** The `first_three` view showed only the first three elements,
but `numbers` still carries five elements at the end of the operation —
its size did not change. `views::take` falls into the same kind as
`views::filter`: **unchanged**. The difference is that `filter` decides
which elements will pass with a condition, while `take` decides only how
many elements will pass with a number. Both build a different window
looking at the underlying `numbers` without writing to it — this is a
shared property of range views: all of them leave the source unchanged,
they only change which part of that source, or in which form, becomes
visible.

#### `views::drop`: Skipping the First N Elements of a Range

The opposite of `std::views::take` is `std::views::drop`: it produces a
view that skips the first `n` elements of a given range and shows the
rest. Neither copies the underlying container; both only build a bound
that determines where traversal starts or stops.

```cpp
// views-drop.cpp — does drop copy the underlying container
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> numbers{10, 20, 30, 40, 50};

    auto last_two = numbers | std::views::drop(3);

    std::printf("last two:");
    for (int value : last_two) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    std::printf("source vector size %zu\n", numbers.size());
    return 0;
}
```

```text
last two: 40 50
source vector size 5
```

**SL37.** The `last_two` view skipped the first three elements (`10, 20,
30`) and showed the remaining two elements (`40, 50`); `numbers` still
carries five elements at the end of the operation. `views::take` and
`views::drop` are two complementary views: one says "show the first n,"
the other says "skip the first n," and both leave the source
**unchanged**. If `numbers | std::views::take(3)` and `numbers |
std::views::drop(3)` are traversed in succession, two complementary but
non-overlapping pieces are obtained (`10, 20, 30` and `40, 50`) — two
different views of the same source that do not overlap with each other.

#### `std::ranges::sort`: A Range Directly, Instead of an Iterator Pair

The `std::sort` seen in the previous lesson asks for an iterator pair
(`begin()`, `end()`). C++20's `std::ranges::sort` does the same job
directly with a range object — there is no need to write
`numbers.begin()`/`numbers.end()`. The result is the same: the container
is reordered in place.

```cpp
// ranges-sort.cpp — ranges::sort does not ask for an iterator pair
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> numbers{5, 3, 1, 4, 2};

    std::ranges::sort(numbers);

    std::printf("sorted:");
    for (int value : numbers) {
        std::printf(" %d", value);
    }
    std::printf("\n");
    return 0;
}
```

```text
sorted: 1 2 3 4 5
```

**SL38.** The call `std::ranges::sort(numbers)` reordered `numbers` in
place — the kind of this operation is **changed**, just like `std::sort`
in the previous lesson. The difference is a convenience in the call:
`std::ranges::sort` accepts a range object directly, making the
`begin()`/`end()` calls internally on its own. This is a shared property
of the algorithms in the `std::ranges` namespace — the same work is
expressed with less code using range objects; the underlying complexity
and the kind of the result (**changed**/**unchanged**) do not change.

#### The Complexity of Constructing a View: O(1); the Complexity of Traversal: Variable

Building a view chain (`numbers | std::views::filter(...) |
std::views::transform(...)`) is constant time — O(1). When this line
runs, no element is visited, no memory is allocated; what is built is
only a definition looking at the source, a series of lambdas, and a few
bound values. The actual work begins when the chain is traversed, and
there the complexity varies by adapter.

Moving to the next element (`operator++`) is O(1) for both
`views::transform` and `views::filter` — but `filter`'s O(1) is an
"amortized" O(1): if the predicate rejects an element, the inner loop
keeps turning until the next accepted element is reached. In the worst
case (if no element passes the predicate), a single `operator++` call
can take O(n); but when the chain is traversed in full, the total cost
is still O(n), because every element is visited at most once.
`views::take` and `views::drop`, on the other hand, are truly O(1):
`take` counts how many elements remain and stops when it reaches that
count; `drop`, on a random-access range (such as `vector`), moves the
starting point forward by `n` steps directly, it does not step through
one by one.

This distinction shows that which adapter is added to a chain has an
invisible effect on performance: construction cost is always O(1), but
traversal cost depends on the adapter's type and the predicate's
selectivity. This lesson's measurement — that the source stays
**unchanged** — is independent of this complexity difference; whether a
view is traversed with O(1) or amortized O(1), it never changes the
underlying container in any case. The complexity difference only affects
how long it takes, not which kind the source falls into.

#### The Bounding Measurement

The central measurement of views in this lesson is **unchanged**: a view
constructed from an lvalue never changes the underlying container at any
stage. Repeating the same construction with a small change — giving the
source not as an lvalue but as an **rvalue** — changes the kind.
`std::move(v)` produces a `std::vector<int>&&`; sending this to
`std::views::transform` brings C++20's owning view
(`std::ranges::owning_view`) into play: the adapter takes the rvalue
given to it into itself **through the move constructor**. The buffer `v`
held is not copied, it passes into the owning view's own member; this
transfer is constant time and does not touch elements one by one. As a
result, `v` remains a moved-from object — the known behavior of
`vector`'s move constructor is to leave behind an empty `vector`, that
is, `v.empty()` now gives `true`. The source's kind drops to **empty**;
whereas if the same chain had been constructed from an lvalue, the kind
would have stayed **unchanged**.

```cpp
// owning-view-from-rvalue.cpp — an rvalue vector is moved into owning_view
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    auto folded = std::move(v) | std::views::transform([](int value) { return value; });

    std::printf("view:");
    for (int value : folded) {
        std::printf(" %d", value);
    }
    std::printf("\n");

    std::printf("is v empty: %s\n", v.empty() ? "true" : "false");
    return 0;
}
```

```text
view: 10 20 30
is v empty: true
```

The view still gives `10 20 30`, because the elements were never copied
anywhere; only who owns them changed. Even though `v` is now empty, the
owning view continues to hold the buffer it moved into itself, and
traversal runs through it. The basis for this emptying is the **move
constructor** — the same result must not be assumed for a move
**assignment**, because assignment already writes over an existing
target, and whether it empties the source is a separate guarantee. The
owning view, as its name says, ties the lifetime of the object it
carries to its own lifetime; this is why the example above is safe and
can be run as code. In contrast, a situation where a view holds *a
reference looking at a container* and that container's lifetime ends
before the view's produces undefined behavior — such an example is not
written or run here, because undefined behavior itself is not this
lesson's subject, only knowing that it exists matters. C++20's
`owning_view` was designed exactly to eliminate this risk: a view
constructed from an rvalue now owns the source itself by carrying it,
not by holding a reference to it.

#### Summary

- A view (`std::views::filter`, `std::views::transform`) does not copy
  the underlying container; the source container comes out of the
  view-construction operation **unchanged**.
- A view chain is evaluated lazily: elements are produced one at a time
  during traversal, not when the chain is built.
- `filter` and `transform` together build a pipeline; the order in the
  chain determines which step is applied first.
- The kind of this lesson's central measurement is **unchanged**: a view
  constructed from an lvalue never changes the underlying container at
  any stage.
- The bounding measurement: when the same construction is done with an
  rvalue instead of an lvalue, the kind drops to **empty** —
  `std::move(v) | std::views::transform(...)` builds an owning view
  (`owning_view`), takes `v` into itself through the move constructor,
  and `v.empty()` becomes `true`; the view still gives `10 20 30`.

#### Next Step

The line seen in this lesson, `auto chain = numbers |
std::views::filter(...) | ...`, never wrote `chain`'s actual type — that
type is an unnamed view type nesting `filter` and `transform`. The next
lesson is this course's closing lesson: how `auto` and `decltype` deduce
types like this, and what type deduction leaves in the source.

### Type Deduction

Source: https://academia.sh/en/courses/cpp-standard-library/type-deduction

How auto drops const and reference, auto&/const auto&/auto&&, decltype and decltype(auto), the copy-versus-reference difference in structured bindings, and what type deduction leaves in the source.

The previous lesson showed that a view leaves the container **unchanged**
when constructed from an lvalue, and turns the container **empty**
through the move constructor when constructed from an rvalue. In both
examples, `auto` was standing somewhere — instead of writing the view's
actual type, we left it to the compiler. This lesson opens up what that
shortcut does: how `auto` deduces an expression's type, in which case it
drops `const` and the reference, and with which spelling it does not
drop them. This course's central question is the same here too: what
does a type deduction leave in the source?

#### What auto Keeps, What It Drops

`auto` deduces a type from an initializer expression; but the type it
deduces is not identical to the expression's type. The rule is this:
when `auto` is written by itself, it **drops the expression's const and
reference**, leaving only the underlying type behind. The example below
shows this.

```cpp
// auto-drop.cpp — how auto drops const and reference
#include <cstdio>

int main() {
    const int constant = 42;
    int value = 10;
    int& reference = value;

    auto a = constant;    // int -- const dropped
    auto b = reference;   // int -- reference dropped

    a = 99;
    b = 99;

    std::printf("constant %d, value %d\n", constant, value);
    std::printf("a %d, b %d\n", a, b);
    return 0;
}
```

```text
constant 42, value 10
a 99, b 99
```

`a` and `b` are each a copy of `constant` and `reference`; writing to
either does not affect the source. `constant` stays at 42, `value` stays
at 10. **SL39.** `auto` produces an independent copy by dropping an
expression's const and reference; writing to the copy leaves nothing in
the source — the source stays **unchanged**, because the copy itself is
a separate object.

#### Binding to a Reference with auto&

When `auto&` is written, the reference does not drop; the deduced type
is a reference bound to the initializer itself. This is behavior
opposite to `auto`: not a copy, but the source itself.

```cpp
// auto-reference.cpp — auto& binds to the source itself, not a copy
#include <cstdio>

int main() {
    int value = 10;
    int& reference = value;

    auto& binding = reference;   // int& -- reference does not drop
    binding = 99;

    std::printf("value %d\n", value);
    return 0;
}
```

```text
value 99
```

The assignment made through `binding` reaches `value`; `auto&` did not
forget that `reference` was a reference. The only difference between
`auto` and `auto&` is a single `&` mark, but the result is reversed: one
builds a copy, the other binds to the source itself.

#### const auto&: Read-Only Without a Copy

While traversing a container just to read it, `auto` copies every
element. If the element is large (a `std::string`, a record), this
copying is an unnecessary cost. `const auto&` binds the source itself in
a read-only form, without taking a copy.

```cpp
// const-auto-reference.cpp — reading a large element without copying it
#include <cstdio>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> stock{"apple", "pear", "cherry"};

    for (const auto& name : stock) {
        std::printf("%s\n", name.c_str());
    }

    std::printf("element count %zu\n", stock.size());
    return 0;
}
```

```text
apple
pear
cherry
element count 3
```

No `std::string` was copied throughout the loop; `name` was bound to
`stock`'s current element at each step. `const` tells the compiler that
no writing will happen through this binding, and `stock` stays
**unchanged** at the end of the loop. Not taking a copy here is a
performance decision, it does not change the effect on the source: both
copying `auto` and `const auto&` leave the source untouched during a
read — the difference is only whether an intermediate copy exists.

#### auto&&: Forwarding Reference

When `auto&&` is written, the deduced type changes according to the
initializer's value category: it becomes an ordinary reference (`T&`)
when initialized with an lvalue, and an rvalue reference (`T&&`) when
initialized with an rvalue. The `auto&&` form that shows this dual
behavior is called a **forwarding reference**.

```cpp
// auto-forwarding-reference.cpp — auto&& deduces a different type by value category
#include <cstdio>
#include <string>

int main() {
    std::string name = "apple";

    auto&& lvalue_binding = name;                     // string&
    auto&& rvalue_binding = std::string("pear");       // string&&

    lvalue_binding += " (updated)";

    std::printf("name: %s\n", name.c_str());
    std::printf("rvalue: %s\n", rvalue_binding.c_str());
    return 0;
}
```

```text
name: apple (updated)
rvalue: pear
```

Because `lvalue_binding` is bound to `name` itself, the append made
through it reaches `name`. `rvalue_binding`, in turn, was bound to a
temporary object and extended its lifetime to the end of the statement.
A forwarding reference only appears through deduction in the form
`auto&&` or a template parameter `T&&`; a hand-written `std::string&&`
does not show this behavior, because its type is already fixed, it is
not deduced.

#### decltype and decltype(auto)

`decltype` follows a different rule from `auto`: it gives an
expression's type **exactly as it is**, without changing it. Where `auto`
drops the reference, `decltype` does not.

```cpp
// decltype-example.cpp — decltype does not drop the reference
#include <cstdio>

int main() {
    int value = 10;
    int& reference = value;

    decltype(value) a = 1;        // int
    decltype(reference) b = value; // int& -- reference does not drop

    b = 42;

    std::printf("value %d, a %d\n", value, a);
    return 0;
}
```

```text
value 42, a 1
```

`b` took the type `int&` thanks to `decltype(reference)` and was bound
to `value` itself; `a`, on the other hand, is an ordinary `int` copy.
`auto` would have reduced both to `int` here.

`decltype(auto)` is used to deduce a function's return type by
`decltype`'s rules — but it carries a trap: **wrapping the return
expression in parentheses** changes the deduced type. The two functions
below appear to return the same variable, but one deduces a value, the
other a reference.

```cpp
// decltype-auto-parentheses.cpp — parentheses turn the expression into an lvalue and change the type
#include <cstdio>
#include <type_traits>

int counter = 0;

decltype(auto) increment_and_return() {
    ++counter;
    return counter;     // decltype(auto) -> int (a variable name by itself)
}

decltype(auto) increment_and_return_parenthesized() {
    ++counter;
    return (counter);   // decltype(auto) -> int& (a parenthesized expression is an lvalue)
}

int main() {
    constexpr bool first_is_value = std::is_same_v<decltype(increment_and_return()), int>;
    constexpr bool second_is_reference =
        std::is_same_v<decltype(increment_and_return_parenthesized()), int&>;

    std::printf("is the first a value: %s\n", first_is_value ? "yes" : "no");
    std::printf("is the second a reference: %s\n", second_is_reference ? "yes" : "no");
    return 0;
}
```

```text
is the first a value: yes
is the second a reference: yes
```

`decltype(auto)` sees `return counter;` as a plain variable name by
itself and deduces `int`. For `return (counter);` it reads the
parenthesized expression as an lvalue and deduces `int&`. Both functions
work safely here because `counter` itself is defined at namespace scope;
but returning a **local** variable defined inside a function the same
way with parentheses produces a reference to an object that no longer
exists once the function ends — undefined behavior is entered even
before the caller reads that reference. This is why the parentheses in a
`decltype(auto)` return are not a spelling choice that can be ignored.

#### Structured Bindings: Copy or Reference

A structured binding splits a record type (a `std::pair`, an element of a
`std::map`) into named parts in a single line: `auto [name, count] :
stock`. The `auto` here follows the same rule — it copies the source; if
you want a reference, you need to add `&`. Because an unordered
container's traversal order is undefined, the example below uses
`std::map`; since it is ordered by key, its output is stable.

```cpp
// structured-binding.cpp — the difference between copying and referencing structured binding
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"apple", 4}, {"pear", 2}};

    for (auto [name, count] : stock) {
        count += 100;   // changes only the copy
    }

    std::printf("after the copy:");
    for (const auto& [name, count] : stock) {
        std::printf(" %s=%d", name.c_str(), count);
    }
    std::printf("\n");

    for (auto& [name, count] : stock) {
        count += 100;   // changes the source itself
    }

    std::printf("after the reference:");
    for (const auto& [name, count] : stock) {
        std::printf(" %s=%d", name.c_str(), count);
    }
    std::printf("\n");
    return 0;
}
```

```text
after the copy: apple=4 pear=2
after the reference: apple=104 pear=102
```

The first loop bound a copy of each element with `auto [name, count]`;
`count += 100` wrote only to that copy, and `stock` appears unchanged on
the "after the copy" line. The second loop bound the elements themselves
with `auto& [name, count]`; the same `+= 100` this time reached `stock`
itself. **SL40.** In structured bindings too, the kind of the type
deduction depends on the `&` before the square brackets: `auto [...]`
leaves the source **unchanged**, `auto& [...]` **changes** the source —
both split the same record type with the same names, the difference is
only in that single character.

#### The Readability Trade-Off

`auto` is a shortcut, but it does not increase readability everywhere.
When the type name tells the reader what the source is and which
operations are valid, it should be written out: writing `auto it` instead
of an iterator's long template name puts the reader at ease, because
`it` itself already says it is an iterator. But when a function's return
type determines the caller's behavior — for example, whether what comes
back is a reference or a copy — hiding this information behind `auto`
pushes the reader toward a wrong assumption. As this lesson has shown,
the difference between `auto` and `auto&` is, for the compiler, a
visible `&` mark, but for the reader, noticing that mark is the only way
to guess whether the source in the code stays **unchanged** or becomes
**changed**. This is why writing `const auto&` in a read-only variable,
`auto&` in a loop that modifies elements, and skipping `auto` altogether
to write out the actual type when that type itself carries meaning,
preserves readability without giving up the shortcut.

M11/K04 ("C++ Fundamentals and Object Design") measured **which object**
an expression works with — the `copy` kind there named a **target**, the
new object an assignment or a constructor writes into. This course's
`unchanged` kind names something entirely different: an operation's
**source**, that is, the object that already exists and is being worked
on. When `for (auto x : v)` builds a copy, there is a single operation
taking place; K04 would ask about that copy's target, this lesson asks
what the operation leaves in the source. The same line gives two
different answers to two different questions.

#### The Bounding Measurement

What this lesson measures is what the deduced type leaves in the source
during container traversal. We traverse the same vector with two
different loops; the only difference in the heading is a single `&`
mark.

```cpp
// auto-reference-writing.cpp — does the auto& loop change the source
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> v{5, 7};
    auto before = v;

    for (auto& x : v) x *= 2;

    std::printf("after auto&:");
    for (int value : v) std::printf(" %d", value);
    std::printf("\n");
    std::printf("did it change: %s\n", v == before ? "no" : "yes");
    return 0;
}
```

```text
after auto&: 10 14
did it change: yes
```

```cpp
// auto-copy-reading.cpp — auto builds a copy, the source does not change
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> v{5, 7};
    auto before = v;
    int total = 0;

    for (auto x : v) {
        total += x;
        x *= 2;   // changes only the copy, does not reach v
    }

    std::printf("auto copy:");
    for (int value : v) std::printf(" %d", value);
    std::printf(", total %d\n", total);
    std::printf("did it change: %s\n", v == before ? "no" : "yes");
    return 0;
}
```

```text
auto copy: 5 7, total 12
did it change: no
```

**SL41.** `for (auto& x : v) x *= 2;` binds to each element through the
source itself; `x *= 2` writes directly to `v`, and `v` **changed** by
the end of the loop. `for (auto x : v)`, in turn, builds a copy at every
step; `x *= 2` changes only that copy, and `v` stays **unchanged** at the
end of the loop — even though `total` was computed from the copies,
nothing was written to the source. The kind of the type deduction is
determined here by a single character in the heading: the compiler sees
the `&` and decides whether to build a reference or a copy, and the
reader has to infer this from the rest of the line. This lesson's kind is
named by the result the `auto&` form gives: **changed**.

#### Summary

- The kind of this lesson's central measurement is **changed**:
  `for (auto& x : v) x *= 2;` writes to the source itself, while
  `for (auto x : v)` changes only a copy and leaves the source
  **unchanged**.
- `auto` drops an expression's const and reference; `auto&` does not
  drop the reference, and `const auto&` reads without taking a copy.
- `auto&&` is a forwarding reference: it deduces `T&` if the initializer
  is an lvalue, `T&&` if it is an rvalue; this behavior appears only in
  deduced contexts.
- `decltype` gives an expression's type exactly as it is, without
  dropping the reference; `decltype(auto)` deduces a function's return
  type by the same rule, and wrapping the return expression in
  parentheses can turn the result from a value into a reference.
- In structured bindings, `auto [name, count]` leaves the source
  **unchanged**, `auto& [name, count]` **changes** the source — again,
  the difference is the single `&` mark before the square brackets.
- Writing out a type name is justified when that name tells the reader
  something about the source's behavior; otherwise, the `auto` shortcut
  does not hurt readability.

#### Course Wrap-Up

Throughout this course we measured eleven different operations with the
same question: what does an operation leave in the source? The table
below summarizes each lesson's measured operation, what is left in the
source, and that lesson's bounding measurement.

| Lesson | Operation measured | Left in the source | Bounding measurement |
| --- | --- | --- | --- |
| Smart Pointers | `unique_ptr` moved | empty | `std::move` alone does not move; the source stays **unchanged** unless the move constructor or move assignment is actually invoked |
| Move Semantics | `std::string` moved | unspecified | `std::move` on a `const` object falls back to the copy constructor, the source stays **unchanged** |
| The Rule of Zero and the Rule of Five | `std::move` on a class with a destructor | unchanged | The same line falls into two different kinds depending on how the class is written: **unchanged** or **unspecified** |
| Non-Copyable and Non-Movable Types | Its own move constructor | empty | `is_move_constructible_v<Lock>` is **false** — the copy constructor is deleted, the move was never declared at all |
| Containers | Reading with `operator[]` on a map | changed | `operator[]` does not compile on a `const map`; it demands write permission even to read |
| Iterators and Validity | An erased iterator | unspecified | The same erase on `std::list` invalidates only the erased node's iterator, the others stay **unchanged** |
| Algorithms | `std::sort` | changed | Both `sort` and `stable_sort` are **changed**, but the order of equal elements is guaranteed in only one |
| The Erase–Remove Idiom | The tail after `std::remove` | unspecified | C++20's free `std::erase` moves directly to **changed** without ever touching the tail |
| Lambdas and Function Objects | Capture by value with `mutable` | unchanged | The same body gives three kinds with three different captures: value **unchanged**, reference **changed**, move **unspecified** |
| Ranges | A view from an lvalue | unchanged | A view constructed from an rvalue turns the source **empty** through the move constructor |
| Type Deduction | A loop with `auto&` | changed | The single `&` between `auto` and `auto&` turns the source from **unchanged** to **changed** |

**3 unchanged · 3 changed · 2 empty · 3 unspecified; 8 with a stated
state, 3 without.** This distribution is the distribution of the eleven
lessons in the table, and it is identical to the distribution of the
core established at the start of the course: every operation either
leaves the source as it is, changes it, empties it by contract, or
leaves it without stating its value, and that value can only be looked
at after it has been given a new one.

The next course is **Templates and Performance**; it covers templates,
concurrency, and performance topics.

