---
title: 'Smart Pointers'
source: 'https://academia.sh/en/courses/cpp-standard-library/smart-pointers'
course: 'Resource Management and the Standard Library'
language: en
updated: '2026-09-26T03:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'distinguishes the ownership difference among unique_ptr, shared_ptr, and weak_ptr'
    - 'explains the state a move operation leaves in the source using the guarantee the standard gives'
    - 'builds an example showing that a std::move call alone moves nothing'
    - 'writes a defined counterpart to a reference cycle leak using weak_ptr'
---

# 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.
