Modern C++ uses smart pointers to make dynamic-memory ownership explicit, connect object lifetime to scope, and reduce the leaks, double deletes, dangling ownership, and cleanup ambiguity associated with unmanaged new and delete.
OSC++.021 continues the object-lifetime sequence from OSC++.020: Virtual Destructors and Polymorphic Cleanup, builds on runtime polymorphism from OSC++.018: Virtual Functions and Polymorphism Basics, and depends on lifetime fundamentals from OSC++.016: Constructors and Destructors Basics.
Learning objectives
- Explain why ownership is a separate concept from pointer syntax.
- Use
std::unique_ptrfor exclusive ownership and transfer ownership with move semantics. - Use
std::shared_ptrwhen lifetime is genuinely shared by multiple owners. - Use
std::weak_ptrto observe shared objects without extending their lifetime and to break reference cycles. - Apply
std::make_uniqueandstd::make_sharedas preferred construction patterns. - Combine smart pointers with polymorphic base classes and virtual destructors.
- Recognize ownership anti-patterns, hidden lifetime coupling, and unnecessary reference counting.
Ownership is the central design question
A pointer stores an address. Ownership describes responsibility for the lifetime of the object at that address. The two concepts are related but not identical. A raw pointer can point to an object without owning it, while a smart pointer can encode ownership directly in its type.
| Type | Typical meaning | Lifetime effect |
|---|---|---|
T* | Non-owning observation, legacy interface, or low-level address use | None by itself |
std::unique_ptr<T> | Exactly one owning handle | Destroys the object when the owner is destroyed or reset |
std::shared_ptr<T> | Shared ownership | Destroys the object when the last owning handle disappears |
std::weak_ptr<T> | Non-owning observation of a shared object | Does not keep the object alive |
The C++ Core Guidelines recommend representing ownership explicitly rather than scattering manual allocation and deallocation across unrelated code. See C++ Core Guidelines R.20–R.24 for the ownership guidance behind modern smart-pointer practice.
The unmanaged pattern and its failure modes
#include <iostream>
struct Sensor {
Sensor() { std::cout << "acquire\n"; }
~Sensor() { std::cout << "release\n"; }
};
void run() {
Sensor* sensor = new Sensor;
// More code executes here.
// Every exit path must eventually perform:
delete sensor;
}
The allocation itself is simple. The lifetime obligation is not. An early return, exception, duplicated delete, reassignment, or unclear ownership transfer can invalidate the cleanup logic. Smart pointers move the cleanup responsibility into an object whose destructor participates in normal C++ scope unwinding.
RAII: lifetime follows scope
Resource Acquisition Is Initialization, commonly abbreviated RAII, binds a resource to an object’s lifetime. When the owner leaves scope, its destructor releases the resource. Smart pointers apply RAII to dynamically allocated objects.
#include <memory>
void run() {
auto sensor = std::make_unique<Sensor>();
// No explicit delete.
// Sensor is destroyed automatically at scope exit.
}
This pattern remains correct across ordinary returns and exception unwinding because the owning smart pointer is itself an automatic object.
Video: RAII and the Rule of Zero
std::unique_ptr: exclusive ownership
std::unique_ptr<T> represents exclusive ownership. One unique_ptr owns the object at a time. Copying is disabled because two independent exclusive owners would contradict the ownership model.
#include <memory>
#include <string>
struct Miner {
explicit Miner(std::string model_name)
: model(std::move(model_name)) {}
std::string model;
};
int main() {
auto miner = std::make_unique<Miner>("S21");
}
cppreference documents std::unique_ptr as an owning smart pointer that manages an object through a stored pointer and associated deleter.
Ownership transfer requires std::move
Exclusive ownership may be transferred, but not copied.
auto first_owner = std::make_unique<Miner>("S21");
// auto second_owner = first_owner; // error: copying disabled
auto second_owner = std::move(first_owner);
// first_owner is now empty.
// second_owner owns the Miner.
Move semantics make the ownership transition visible in source code. After the move, the source unique_ptr remains a valid smart-pointer object but does not own the transferred object.
Passing unique ownership into a function
#include <memory>
#include <vector>
class Fleet {
public:
void add(std::unique_ptr<Miner> miner) {
miners.push_back(std::move(miner));
}
private:
std::vector<std::unique_ptr<Miner>> miners;
};
int main() {
Fleet fleet;
auto miner = std::make_unique<Miner>("S21");
fleet.add(std::move(miner));
}
Taking std::unique_ptr<T> by value communicates that the function accepts ownership. A non-owning function that merely uses the object usually does not need ownership at all; it can take T&, const T&, or an appropriate non-owning pointer.
Factories naturally return unique_ptr
#include <memory>
#include <string>
class Driver {
public:
virtual void poll() = 0;
virtual ~Driver() = default;
};
class S21Driver : public Driver {
public:
void poll() override {}
};
std::unique_ptr<Driver> make_driver(const std::string& model) {
if (model == "S21") {
return std::make_unique<S21Driver>();
}
return nullptr;
}
A factory that creates one object for one caller commonly returns std::unique_ptr. The caller receives ownership explicitly, and polymorphic destruction remains correct because the base class has a virtual destructor.
Video: C++ smart pointers from first principles
std::unique_ptr, std::shared_ptr, ownership semantics, and practical selection guidelines.std::shared_ptr: shared ownership
std::shared_ptr<T> allows multiple owners to participate in the lifetime of one object. The object remains alive while at least one owning shared_ptr exists.
#include <memory>
struct TelemetryBus {};
int main() {
auto bus_a = std::make_shared<TelemetryBus>();
auto bus_b = bus_a;
// bus_a and bus_b share ownership.
}
cppreference documents std::shared_ptr as shared ownership managed through a control block containing ownership bookkeeping such as the strong-reference count.
Reference counting is a cost and a design signal
Shared ownership is more expensive and more semantically complex than exclusive ownership. A typical shared_ptr implementation maintains a control block and updates ownership counts as shared handles are copied or destroyed. The correct question is not whether shared_ptr is convenient; it is whether several independent parts of the program truly share responsibility for keeping the object alive.
- Use
unique_ptrwhen one owner is sufficient. - Use
shared_ptrwhen lifetime responsibility is genuinely shared. - Do not choose
shared_ptrmerely to avoid deciding who owns an object. - Do not assume that reference counting makes the pointed-to object itself thread-safe.
make_shared and allocation efficiency
auto bus = std::make_shared<TelemetryBus>();
std::make_shared is generally the preferred construction form when creating a new object directly into shared ownership. It centralizes construction and can allow the object and control-block bookkeeping to be allocated together.
std::weak_ptr: observation without ownership
std::weak_ptr<T> observes an object managed by shared_ptr without increasing the strong ownership count. A weak pointer must be converted temporarily into a shared_ptr before the object is safely used.
#include <iostream>
#include <memory>
struct Controller {
void status() const {
std::cout << "online\n";
}
};
int main() {
std::weak_ptr<Controller> observer;
{
auto owner = std::make_shared<Controller>();
observer = owner;
if (auto locked = observer.lock()) {
locked->status();
}
}
if (observer.expired()) {
std::cout << "controller lifetime ended\n";
}
}
cppreference documents std::weak_ptr as a non-owning reference to an object managed by std::shared_ptr.
Why weak_ptr is required for reference cycles
Reference counting alone cannot reclaim objects that keep one another alive in a closed cycle.
#include <memory>
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> previous;
};
If two nodes own each other through shared_ptr, external owners may disappear while the internal strong references keep both counts above zero. One relationship should instead be non-owning when it does not represent lifetime responsibility.
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> previous;
};
The weak back-reference preserves navigability without extending the predecessor’s lifetime.
Video: unique_ptr, shared_ptr, and weak_ptr in code
std::unique_ptr, std::shared_ptr, and std::weak_ptr in a focused programming walkthrough.Ownership and polymorphism
Smart pointers and polymorphism work together naturally when the ownership model and destructor model agree.
#include <iostream>
#include <memory>
#include <vector>
class Equipment {
public:
virtual void poll() const = 0;
virtual ~Equipment() = default;
};
class Pdu : public Equipment {
public:
void poll() const override {
std::cout << "PDU telemetry\n";
}
};
class CoolingUnit : public Equipment {
public:
void poll() const override {
std::cout << "Cooling telemetry\n";
}
};
int main() {
std::vector<std::unique_ptr<Equipment>> equipment;
equipment.push_back(std::make_unique<Pdu>());
equipment.push_back(std::make_unique<CoolingUnit>());
for (const auto& item : equipment) {
item->poll();
}
}
The vector exclusively owns heterogeneous objects through one base interface. The collection determines lifetime, unique_ptr performs automatic cleanup, and the virtual destructor ensures destruction reaches the correct derived type.
Non-owning access should stay non-owning
A function that only reads or modifies an object temporarily should not usually receive a smart pointer merely because the caller stores the object in one.
void print_status(const Equipment& equipment) {
equipment.poll();
}
int main() {
auto pdu = std::make_unique<Pdu>();
print_status(*pdu);
}
The function receives exactly the capability it needs: access to an existing object. It does not claim ownership and cannot accidentally extend or transfer lifetime.
get(), reset(), and release()
| Operation | Meaning | Ownership warning |
|---|---|---|
ptr.get() | Returns the stored raw pointer | The smart pointer still owns the object |
ptr.reset() | Destroys the currently owned object and optionally takes a new one | Ownership remains inside the smart pointer model |
ptr.release() | Relinquishes ownership and returns the raw pointer | The caller now bears manual lifetime responsibility |
release() is intentionally different from reset(). Releasing without immediately transferring the returned raw pointer to another well-defined owner can reintroduce the exact lifetime hazards that smart pointers were meant to prevent.
Choosing the pointer type
| Design requirement | Preferred representation |
|---|---|
| Automatic object with ordinary scope lifetime | Object by value; no pointer required |
| One dynamic owner | std::unique_ptr<T> |
| Several true lifetime owners | std::shared_ptr<T> |
| Observe a shared object without keeping it alive | std::weak_ptr<T> |
| Temporary guaranteed access | T& or const T& |
| Nullable, non-owning observation where pointer semantics are appropriate | T* or const T* |
Data-center control example
#include <memory>
#include <string>
#include <unordered_map>
class DeviceSession {
public:
explicit DeviceSession(std::string host)
: host_(std::move(host)) {}
const std::string& host() const { return host_; }
private:
std::string host_;
};
class SessionManager {
public:
void connect(std::string id, std::string host) {
sessions_[std::move(id)] =
std::make_unique<DeviceSession>(std::move(host));
}
const DeviceSession* find(const std::string& id) const {
auto it = sessions_.find(id);
if (it == sessions_.end()) {
return nullptr;
}
return it->second.get();
}
private:
std::unordered_map<std::string,
std::unique_ptr<DeviceSession>> sessions_;
};
The manager is the sole owner of each session. Callers may obtain a non-owning pointer for immediate use, but the manager remains the authority over session lifetime.
Shared telemetry example
#include <memory>
class TelemetryStream {};
class Dashboard {
public:
explicit Dashboard(std::shared_ptr<TelemetryStream> stream)
: stream_(std::move(stream)) {}
private:
std::shared_ptr<TelemetryStream> stream_;
};
class AlertEngine {
public:
explicit AlertEngine(std::shared_ptr<TelemetryStream> stream)
: stream_(std::move(stream)) {}
private:
std::shared_ptr<TelemetryStream> stream_;
};
int main() {
auto stream = std::make_shared<TelemetryStream>();
Dashboard dashboard(stream);
AlertEngine alerts(stream);
}
This design is justified only if the dashboard and alert engine independently participate in keeping the telemetry stream alive. If one component clearly owns the stream and the other merely uses it temporarily, references or non-owning pointers would communicate the design more accurately.
Common mistakes
- Using
shared_ptrby default because ownership has not been designed. - Calling
newdirectly whenmake_uniqueormake_sharedexpresses the intended construction more clearly. - Passing smart pointers to functions that do not participate in ownership.
- Calling
release()and then forgetting that manual ownership has returned. - Creating
shared_ptrcycles without aweak_ptrrelationship. - Constructing separate
shared_ptrowners from the same raw pointer, which can create independent control blocks and double deletion. - Assuming
shared_ptrmakes the object it points to inherently thread-safe. - Using
std::unique_ptr<Base>for a derived object when the base destructor is not appropriate for polymorphic destruction. - Using dynamic allocation when an ordinary value object would be simpler.
Exercises
- Create an abstract
Sensorbase class with a virtualread()function and a virtual defaulted destructor. - Create
TemperatureSensorandPressureSensorderived classes. - Store both objects in
std::vector<std::unique_ptr<Sensor>>. - Write a factory function that returns
std::unique_ptr<Sensor>based on a string or enum selection. - Move the returned pointer into the vector and verify that the source owner becomes empty.
- Create a separate telemetry object owned by two independent consumers with
std::shared_ptr. - Add a monitoring object that observes the telemetry object with
std::weak_ptrand useslock()before access. - Create a two-node shared-ownership cycle, observe why the objects stay alive, then replace one direction with
weak_ptr. - Refactor one function that accepts
shared_ptr<T>unnecessarily so it acceptsconst T&instead.
Knowledge check
1. What does std::unique_ptr represent?
Exclusive ownership of a dynamically managed object. The owned object is destroyed automatically when the owning smart pointer is destroyed or reset.
2. Why can a unique_ptr be moved but not copied?
Moving transfers the single ownership responsibility. Copying would create two exclusive owners and violate the type’s ownership model.
3. When is shared_ptr appropriate?
When multiple independent program components genuinely share responsibility for keeping one object alive.
4. What does weak_ptr contribute?
It observes an object managed by shared_ptr without increasing the strong ownership count and can break cycles in shared-ownership graphs.
5. What does weak_ptr::lock() do?
It attempts to create a temporary shared_ptr if the observed object is still alive. If the object has expired, the returned shared_ptr is empty.
6. Why is make_unique normally preferred over writing unique_ptr<T>(new T(...))?
It expresses construction directly, reduces manual ownership syntax, and keeps allocation tied to the owning smart-pointer creation.
7. Does shared_ptr make the pointed-to object thread-safe?
No. Ownership bookkeeping and access to the object’s own mutable state are different concerns.
8. Why can std::unique_ptr<Base> still require a virtual base destructor?
Because the smart pointer may destroy a derived object through the base type. The base-class destruction interface must support the intended polymorphic cleanup.
Key takeaway
Smart pointers are ownership types, not merely safer pointer syntax. std::unique_ptr should be the default owning pointer when one owner is sufficient, std::shared_ptr should be reserved for true shared lifetime, and std::weak_ptr provides non-owning observation inside shared-ownership systems. When ownership is made explicit, cleanup becomes a property of the type system and object lifetime rather than a scattered manual convention.
Technical note: The examples use C++17-compatible smart-pointer patterns except where standard-library behavior is described generically. Production code should compile with project-specific warning levels, static analysis, sanitizers, and the language standard selected by the build system.

Leave a comment