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.
| Relationship | Typical representation |
|---|---|
| Contained value | Direct data member |
| Exclusive dynamic child | std::unique_ptr<T> |
| Required borrowed collaborator | T& |
| Optional borrowed observer | T* |
| Independent shared owners | std::shared_ptr<T> |
| Observation of shared lifetime | std::weak_ptr<T> |
Key idea
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.
#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
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.
#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
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.
#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.
#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.
#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 togetherWatch out
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.