Ownership: the question this chapter kept asking

Every lesson in this chapter circles a single decision. When a resource travels from one part of your program to another, should the program duplicate it, hand it over, or merely look at it? Copy semantics duplicate. Move semantics hand over. Observers only look.

Instead of replaying the lessons one at a time, this recap is organised around that decision, because that is the shape the knowledge takes once you start writing real classes.

What each lesson contributed

Lesson The decision it covered What to carry forward
Move Semantics Terminology Vocabulary Value categories, moved-from state, and the special member functions, in one lookup table
Automatic Memory Management Who is responsible for delete A smart pointer is a composition class that owns a heap allocation and releases it from its destructor when the smart pointer goes out of scope
Understanding Rvalue References How the compiler tells "duplicate" from "hand over" T&& is a reference that binds only to r-values, which is the signal that makes move overloads selectable
Implementing Move Semantics How to write the hand-over A move constructor and move assignment operator steal the source's pointer and leave the source empty but valid
Casting to Rvalue References Forcing a hand-over on a named object std::move is a cast, not an operation
Exclusive Ownership Smart Pointers Exactly one owner std::unique_ptr deletes copying, enables moving, and is built with std::make_unique()
Shared Ownership Smart Pointers Several owners at once std::shared_ptr counts its owners and destroys the resource when the last one goes away
Solving Circular References with std::weak_ptr Owners that trap each other std::weak_ptr observes a shared resource without being counted as an owner

L-values and r-values decide what is safe

The compiler does not guess whether stealing is acceptable. It reads the value category of the argument and applies a rule that is always safe.

Argument Examples What the compiler is entitled to assume Overload it selects
l-value kick, array[2], *handle The object has a name, so later code may still read it Copy
r-value AudioClip{"tom", 120}, makeClip(), std::move(kick) The object is a temporary, or you have declared yourself finished with it Move

That is the whole justification for move semantics. A temporary is destroyed at the end of the full expression that created it, so reusing its buffer costs nothing and breaks nothing. A named object might be read on the next line, so the only safe default is to duplicate.

Warning
A variable of r-value reference type is itself an l-value. Given AudioClip&& incoming, the parameter incoming has a name, so passing it along copies unless you write std::move(incoming).

The four hand-over functions in one program

This class implements all four so you can watch the compiler choose between them.

#include <iostream>
#include <string>
#include <string_view>
#include <utility>

class AudioClip
{
public:
    AudioClip(std::string_view title, int frames)
        : m_title{title}
        , m_frames{frames}
        , m_samples{new double[static_cast<std::size_t>(frames)]{}}
    {
        std::cout << "built " << m_title << '\n';
    }

    ~AudioClip()
    {
        delete[] m_samples;
    }

    AudioClip(const AudioClip& source)
        : m_title{source.m_title}
        , m_frames{source.m_frames}
        , m_samples{new double[static_cast<std::size_t>(source.m_frames)]{}}
    {
        std::cout << "copied " << m_title << '\n';
    }

    AudioClip& operator=(const AudioClip& source)
    {
        if (this == &source)
            return *this;

        delete[] m_samples;
        m_title = source.m_title;
        m_frames = source.m_frames;
        m_samples = new double[static_cast<std::size_t>(source.m_frames)]{};

        std::cout << "copy assigned " << m_title << '\n';
        return *this;
    }

    AudioClip(AudioClip&& source) noexcept
        : m_title{std::move(source.m_title)}
        , m_frames{source.m_frames}
        , m_samples{source.m_samples}
    {
        source.m_frames = 0;
        source.m_samples = nullptr;

        std::cout << "handed over " << m_title << '\n';
    }

    AudioClip& operator=(AudioClip&& source) noexcept
    {
        if (this == &source)
            return *this;

        delete[] m_samples;
        m_title = std::move(source.m_title);
        m_frames = source.m_frames;
        m_samples = source.m_samples;
        source.m_frames = 0;
        source.m_samples = nullptr;

        std::cout << "move assigned " << m_title << '\n';
        return *this;
    }

    int frames() const { return m_frames; }

private:
    std::string m_title{};
    int m_frames{0};
    double* m_samples{nullptr};
};

int main()
{
    AudioClip kick{"kick", 240};
    AudioClip snare{"snare", 480};

    AudioClip backup{kick};              // argument is an l-value: copy constructor
    AudioClip carried{std::move(snare)}; // argument is an r-value: move constructor

    AudioClip slot{"silence", 8};
    slot = backup;             // argument is an l-value: copy assignment
    slot = std::move(carried); // argument is an r-value: move assignment

    std::cout << "slot now holds " << slot.frames() << " frames" << '\n';

    return 0;
}

Output:

built kick
built snare
copied kick
handed over snare
built silence
copy assigned kick
move assigned snare
slot now holds 480 frames

The two copy paths call new a second time. The two move paths only reassign a pointer and blank the source, which is why moving is cheap no matter how large the buffer is.

Function Signature shape Job
Copy constructor AudioClip(const AudioClip&) Build a new object with its own allocation
Copy assignment AudioClip& operator=(const AudioClip&) Release what we hold, then allocate and duplicate
Move constructor AudioClip(AudioClip&&) noexcept Build a new object from the source's allocation, then empty the source
Move assignment AudioClip& operator=(AudioClip&&) noexcept Release what we hold, adopt the source's allocation, then empty the source
Best Practice
Mark move operations noexcept, guard both assignment operators against self-assignment, and always leave the emptied source in a state that is safe to destroy or reassign.
Key Concept
The compiler will not write a correct move constructor for a class holding a raw owning pointer. An implicitly generated move simply copies the pointer, leaving two objects that both call delete. Either write the move operations yourself or hold the resource in a member that already manages itself.

std::move changes the label, not the data

std::move performs no moving at all. It is a cast that relabels an l-value as an r-value so that overload resolution reaches the move overload. Nothing has been transferred until a move constructor or move assignment operator actually runs.

Warning
Apply std::move only to a persistent object whose value you no longer need. After the move the object is in a valid but unspecified state: destroying it or assigning a fresh value to it is fine, reading its old value is not.

Picking an owner

Situation Reach for Create it with Copyable Movable
One owner, no sharing std::unique_ptr std::make_unique() No, copying is deleted Yes
Several owners, resource lives until the last one goes std::shared_ptr std::make_shared() Yes, and copying is how you share Yes
Access to a shared resource without extending its life std::weak_ptr Copy from a std::shared_ptr Yes Yes
A function that only uses a resource it does not own Plain reference or raw pointer Nothing to create n/a n/a
Anything at all Never std::auto_ptr It was deprecated and then removed in C++17 n/a n/a

Moving a std::unique_ptr is the clearest demonstration of the whole chapter, because the class deletes its copy operations, so a transfer is the only thing that can happen.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

class MixTrack
{
public:
    explicit MixTrack(std::string_view label)
        : m_label{label}
    {
        std::cout << "track " << m_label << " opened" << '\n';
    }

    ~MixTrack()
    {
        std::cout << "track " << m_label << " closed" << '\n';
    }

    const std::string& label() const { return m_label; }

private:
    std::string m_label{};
};

int main()
{
    std::cout << std::boolalpha;

    std::unique_ptr<MixTrack> owner{std::make_unique<MixTrack>("vocals")};
    std::unique_ptr<MixTrack> newOwner{std::move(owner)};

    std::cout << "old handle empty: " << (owner == nullptr) << '\n';
    std::cout << "new handle holds " << newOwner->label() << '\n';

    std::shared_ptr<MixTrack> firstShare{std::make_shared<MixTrack>("drums")};
    {
        std::shared_ptr<MixTrack> secondShare{firstShare};
        std::cout << "owners inside block: " << firstShare.use_count() << '\n';
    }
    std::cout << "owners after block: " << firstShare.use_count() << '\n';

    return 0;
}

Output:

track vocals opened
old handle empty: true
new handle holds vocals
track drums opened
owners inside block: 2
owners after block: 1
track drums closed
track vocals closed

Two facts are worth pinning down from that run. Moving a std::unique_ptr leaves the original handle holding nullptr, so testing it is well defined and dereferencing it is not. And a std::shared_ptr copy raises the owner count while it exists and lowers it again on destruction, with the tracked object surviving until the count reaches zero.

Best Practice
Build shared ownership by copying an existing std::shared_ptr, never by handing the same raw pointer to a second one. Two independently created owners each believe they are the only owner, and both will free the same memory.

When shared ownership turns into a leak

Reference counting has one failure mode: a group of std::shared_ptr members that reference each other keeps its own count above zero forever.

The program below is broken. It never destroys either object.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>

class MixDesk
{
public:
    explicit MixDesk(std::string_view name)
        : m_name{name}
    {
    }

    ~MixDesk()
    {
        std::cout << m_name << " shut down" << '\n';
    }

    void pairWith(const std::shared_ptr<MixDesk>& partner)
    {
        m_partner = partner;
    }

private:
    std::string m_name{};
    std::shared_ptr<MixDesk> m_partner{};
};

int main()
{
    auto deck{std::make_shared<MixDesk>("deck")};
    auto rack{std::make_shared<MixDesk>("rack")};

    deck->pairWith(rack);
    rack->pairWith(deck);

    std::cout << "leaving main" << '\n';

    return 0;
}

Output:

leaving main

Neither destructor message appears. When main ends, each object is still held by the other's m_partner, so neither count ever falls to zero and the memory is never released.

Switching the back reference to a std::weak_ptr fixes it. A weak pointer does not participate in the count, so it cannot keep anything alive. To use one you call lock(), which returns a std::shared_ptr that is empty if the resource is already gone.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>

class MixDesk
{
public:
    explicit MixDesk(std::string_view name)
        : m_name{name}
    {
    }

    ~MixDesk()
    {
        std::cout << m_name << " shut down" << '\n';
    }

    void pairWith(const std::shared_ptr<MixDesk>& partner)
    {
        m_partner = partner;
    }

    void report() const
    {
        std::shared_ptr<MixDesk> partner{m_partner.lock()};

        if (partner)
            std::cout << m_name << " is paired with " << partner->m_name << '\n';
        else
            std::cout << m_name << " has no partner left" << '\n';
    }

private:
    std::string m_name{};
    std::weak_ptr<MixDesk> m_partner{};
};

int main()
{
    auto deck{std::make_shared<MixDesk>("deck")};
    auto rack{std::make_shared<MixDesk>("rack")};

    deck->pairWith(rack);
    rack->pairWith(deck);

    deck->report();
    rack.reset();
    deck->report();

    std::cout << "leaving main" << '\n';

    return 0;
}

Output:

deck is paired with rack
rack shut down
deck has no partner left
leaving main
deck shut down

Both objects are now released, and the surviving one notices that its partner has gone. Where you only need a yes-or-no answer, expired() reports whether the observed resource is still alive without producing a std::shared_ptr.

Pitfalls the chapter warned about

Mistake What goes wrong Fix
Reading a moved-from object's value The state is valid but unspecified, so the value is not yours to rely on Assign a fresh value first, or do not move until you are finished
Relying on the implicit move for a raw owning pointer The pointer is copied, and two objects free the same memory Write the move operations, or store the resource in a self-managing member
Building a second std::shared_ptr from a raw pointer Two separate counts, one allocation, one double free Copy the existing std::shared_ptr
Allocating with new and then wrapping it Loses the exception-safety guarantee that the factory functions give you std::make_unique() and std::make_shared()
Mutual std::shared_ptr members The cycle keeps its own count alive and the memory leaks Make one direction a std::weak_ptr
Returning an r-value reference Almost always leaves the caller with a reference to something already destroyed Return by value and let move semantics handle the cost

Summary

  • A smart pointer is a composition class that owns heap memory and frees it from its destructor when it goes out of scope.
  • Copy semantics duplicate a resource, move semantics transfer ownership of it, and the argument's value category decides which one the compiler picks.
  • R-value references, written T&&, bind only to r-values and are what make the move overloads reachable.
  • std::move is a cast that lets you invoke move semantics on an l-value you have finished with.
  • Deleting the copy constructor and copy assignment operator turns copying into a compile error, which is exactly how std::unique_ptr enforces exclusive ownership.
  • Use std::unique_ptr for a single owner, std::shared_ptr for shared ownership by reference count, and std::weak_ptr to observe a shared resource without owning it.
  • Prefer std::make_unique() and std::make_shared() over wrapping the result of new.

Key Terminology

  • Smart pointer: Composition class that owns a dynamic allocation and releases it in its destructor
  • RAII: Tying a resource's lifetime to the lifetime of an object, so cleanup happens on scope exit
  • Copy semantics: The rules that duplicate an object, implemented by the copy constructor and copy assignment operator
  • Move semantics: The rules that transfer ownership, implemented by the move constructor and move assignment operator
  • l-value: An expression naming an object that persists beyond the expression
  • r-value: A temporary expression with no name of its own
  • r-value reference: A reference declared with && that binds only to r-values
  • std::move: A cast that relabels an l-value as an r-value so move overloads become viable
  • Move constructor: Builds a new object out of another object's resources
  • Move assignment operator: Replaces an existing object's resources with another object's resources
  • Moved-from state: The valid but unspecified condition of a source object after its resources are taken
  • noexcept: A promise that a function will not throw, which the library relies on before choosing to move
  • Deleted function: A member marked = delete, used here to switch copying off entirely
  • std::auto_ptr: An early ownership type, deprecated in C++11 and removed in C++17
  • std::unique_ptr: Exclusive-ownership smart pointer with copying deleted and moving enabled
  • std::make_unique: The preferred factory for std::unique_ptr
  • std::shared_ptr: Shared-ownership smart pointer that destroys the resource when its last owner goes away
  • std::make_shared: The preferred factory for std::shared_ptr
  • Reference counting: The tally of how many std::shared_ptr currently own a resource
  • std::weak_ptr: Non-owning observer of a std::shared_ptr resource, accessed through lock()
  • Circular reference: A loop of owning pointers whose counts keep each other above zero

Looking Forward

You can now answer the ownership question for any resource your code touches: one owner, several owners, or a plain observer. That answer drives everything else, including which special member functions you write, which smart pointer you reach for, and where a leak could hide. Classes that get it right hand large buffers around at the cost of a pointer assignment and never need a manual delete.

The next chapter turns from a single object's resources to how objects relate to one another. Composition, aggregation, association, and dependency each imply a different ownership answer, and the tools from this chapter are how you express that answer in code.