Phase 4 · Advanced C++ & Systems ProgrammingModule 21~58 min read

Smart Pointers, Ownership Graphs & Allocators

Represent complex ownership with smart pointers, avoid shared cycles, and control allocation through polymorphic memory resources.

What you'll learn

Smart pointers are ownership types, not automatic replacements for every raw pointer. You will map exclusive and shared ownership, break cycles with weak observation, wrap custom resources, and use polymorphic memory resources when allocation strategy is a measured design concern.

By the end, you'll be able to:

  • Express exclusive ownership and polymorphic destruction with unique_ptr
  • Explain shared_ptr control blocks, aliasing, and weak_ptr cycles
  • Attach custom deletion to non-memory resources
  • Use std::pmr to separate container behavior from allocation strategy

Draw the ownership map

Before selecting a pointer type, identify which object controls each lifetime. A solid graph has clear owner edges, observer edges, and destruction order. Most nodes should have one value or unique_ptr owner; shared ownership is reserved for genuinely independent owners that cannot nominate one lifetime authority.

RelationshipTypical representation
Contained valueDirect data member
Exclusive dynamic childstd::unique_ptr<T>
Required borrowed collaboratorT&
Optional borrowed observerT*
Independent shared ownersstd::shared_ptr<T>
Observation of shared lifetimestd::weak_ptr<T>

Key idea

Pointer type should communicate the edge in the ownership graph. If reviewers cannot tell who destroys an object, the design is incomplete.

Exclusive and polymorphic ownership

unique_ptr has near-raw-pointer size in the common default-deleter case and transfers by move. It supports arrays, custom deleters, and conversion fromunique_ptr<Derived> to unique_ptr<Base> when base destruction is safe.

unique_polymorphism.cpp
#include <iostream>
#include <memory>
#include <vector>

class Task {
public:
    virtual void run() = 0;
    virtual ~Task() = default;
};

class Backup final : public Task {
public:
    void run() override { std::cout << "backup\n"; }
};

int main() {
    std::vector<std::unique_ptr<Task>> tasks;
    tasks.push_back(std::make_unique<Backup>());
    for (auto& task : tasks) task->run();
}

Tip

Accept unique_ptr<T> by value only to take ownership. A function that merely uses the object should accept T&, const T&, or an optional T*.

shared_ptr and control blocks

A shared_ptr contains an observed pointer and participates in a control block that tracks strong and weak counts plus deletion state. make_shared commonly allocates object and control block together. Copying changes reference counts, often with atomic operations; it is not a free or universal lifetime solution.

shared_document.cpp
#include <iostream>
#include <memory>
#include <string>

struct Document { std::string title; };

int main() {
    auto document{std::make_shared<Document>(Document{"Guide"})};
    auto editor_view{document};
    auto preview_view{document};

    std::cout << document->title << '\n';
    std::cout << "owners: " << document.use_count() << '\n';
}

Watch out

Do not use use_count() to make synchronization or uniqueness decisions; the count can change concurrently and often reveals a missing ownership decision.

weak_ptr and cycles

Strong ownership cycles never reach a zero strong count. A weak_ptr observes an object managed by shared_ptr without extending its lifetime. Callinglock() atomically produces a temporary shared owner when the object still exists.

weak_parent.cpp
#include <iostream>
#include <memory>
#include <string>

struct Parent;
struct Child { std::weak_ptr<Parent> parent; };
struct Parent { std::string name; std::shared_ptr<Child> child; };

int main() {
    auto parent{std::make_shared<Parent>()};
    parent->name = "root";
    parent->child = std::make_shared<Child>();
    parent->child->parent = parent;

    if (auto owner{parent->child->parent.lock()}) {
        std::cout << owner->name << '\n';
    }
}

Note

enable_shared_from_this lets an already shared-owned object obtain another shared owner safely. Calling shared_ptr<T>(this) creates a competing control block and can cause double deletion.

Custom deleters and native resources

Smart-pointer deletion can call something other than delete. A custom deleter makes C file handles, library objects, and operating-system handles scope-bound. The deleter type is part of unique_ptr's type but is stored inside a shared control block forshared_ptr.

file_owner.cpp
#include <cstdio>
#include <memory>
#include <stdexcept>

struct CloseFile {
    void operator()(std::FILE* file) const noexcept {
        if (file) std::fclose(file);
    }
};
using File = std::unique_ptr<std::FILE, CloseFile>;

File open_file(const char* path) {
    File file{std::fopen(path, "rb")};
    if (!file) throw std::runtime_error{"open failed"};
    return file;
}

Allocators and std::pmr

Allocator-aware containers separate element behavior from memory acquisition. The polymorphic-memory-resource library offers runtime-selectable strategies behindstd::pmr::memory_resource. A monotonic resource allocates quickly in batches and releases everything together—useful for request- or frame-lifetime data.

pmr.cpp
#include <array>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <vector>

int main() {
    std::array<std::byte, 1024> storage{};
    std::pmr::monotonic_buffer_resource arena{storage.data(), storage.size()};
    std::pmr::vector<int> values{&arena};

    for (int value{}; value < 10; ++value) values.push_back(value * value);
    std::cout << values.back() << '\n';
} // vector dies, then the arena releases its allocations together

Watch out

A PMR container must not outlive its memory resource. Allocation tuning is an advanced optimization; measure allocation cost before making resource lifetime part of an API.

Recap & quick check

Key takeaways

  • Ownership graphs distinguish owners from observers and make destruction order reviewable.
  • unique_ptr is the default dynamic owner and supports safe polymorphism through a virtual base destructor.
  • shared_ptr shares a control block and should represent genuinely independent lifetime owners.
  • weak_ptr observes shared lifetime without creating strong cycles.
  • std::pmr changes allocation strategy, but its resource must outlive every using container.

Quick check

1. Which type best represents one dynamic owner?

2. Where are shared_ptr reference counts stored?

3. How should a shared ownership cycle normally be broken?

4. What lifetime rule applies to a pmr container?

Next: Module 22 — Compile-Time Programming & Metaprogramming, where types and values drive validated computation before runtime.