OSC++.021: Smart Pointers — unique_ptr, shared_ptr, weak_ptr, and Ownership

Black and neon-green C++ workstation showing unique_ptr, shared_ptr, and weak_ptr ownership concepts

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_ptr for exclusive ownership and transfer ownership with move semantics.
  • Use std::shared_ptr when lifetime is genuinely shared by multiple owners.
  • Use std::weak_ptr to observe shared objects without extending their lifetime and to break reference cycles.
  • Apply std::make_unique and std::make_shared as 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.

TypeTypical meaningLifetime effect
T*Non-owning observation, legacy interface, or low-level address useNone by itself
std::unique_ptr<T>Exactly one owning handleDestroys the object when the owner is destroyed or reset
std::shared_ptr<T>Shared ownershipDestroys the object when the last owning handle disappears
std::weak_ptr<T>Non-owning observation of a shared objectDoes 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

CppCon — Arthur O’Dwyer, “Back to Basics: RAII and the Rule of Zero.” Explains resource lifetime, leak prevention, double-free prevention, and how RAII supports predictable cleanup.

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

CppCon — David Olsen, “Back to Basics: C++ Smart Pointers.” Covers 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_ptr when one owner is sufficient.
  • Use shared_ptr when lifetime responsibility is genuinely shared.
  • Do not choose shared_ptr merely 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

The Cherno — “SMART POINTERS in C++.” Demonstrates practical use of 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()

OperationMeaningOwnership warning
ptr.get()Returns the stored raw pointerThe smart pointer still owns the object
ptr.reset()Destroys the currently owned object and optionally takes a new oneOwnership remains inside the smart pointer model
ptr.release()Relinquishes ownership and returns the raw pointerThe 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 requirementPreferred representation
Automatic object with ordinary scope lifetimeObject by value; no pointer required
One dynamic ownerstd::unique_ptr<T>
Several true lifetime ownersstd::shared_ptr<T>
Observe a shared object without keeping it alivestd::weak_ptr<T>
Temporary guaranteed accessT& or const T&
Nullable, non-owning observation where pointer semantics are appropriateT* 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_ptr by default because ownership has not been designed.
  • Calling new directly when make_unique or make_shared expresses 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_ptr cycles without a weak_ptr relationship.
  • Constructing separate shared_ptr owners from the same raw pointer, which can create independent control blocks and double deletion.
  • Assuming shared_ptr makes 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

  1. Create an abstract Sensor base class with a virtual read() function and a virtual defaulted destructor.
  2. Create TemperatureSensor and PressureSensor derived classes.
  3. Store both objects in std::vector<std::unique_ptr<Sensor>>.
  4. Write a factory function that returns std::unique_ptr<Sensor> based on a string or enum selection.
  5. Move the returned pointer into the vector and verify that the source owner becomes empty.
  6. Create a separate telemetry object owned by two independent consumers with std::shared_ptr.
  7. Add a monitoring object that observes the telemetry object with std::weak_ptr and uses lock() before access.
  8. Create a two-node shared-ownership cycle, observe why the objects stay alive, then replace one direction with weak_ptr.
  9. Refactor one function that accepts shared_ptr<T> unnecessarily so it accepts const 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.

2 responses to “OSC++.021: Smart Pointers — unique_ptr, shared_ptr, weak_ptr, and Ownership”

  1. […] Modern C++ move semantics lets programs transfer ownership of resources instead of unnecessarily duplicating them. OSC++.022 follows OSC++.021 smart pointers and ownership by explaining the language machinery behind efficient ownership transfer: rvalue references, std::move, move constructors, move assignment, and valid moved-from states. These mechanisms are especially important for resource-owning software objects that manage heap memory, file handles, sockets, buffers, and other nontrivial resources. […]

    Like

  2. […] from one object to another instead of performing an expensive deep copy. OSC++.022 follows OSC++.021: Smart Pointers — unique_ptr, shared_ptr, weak_ptr, and Ownership, where std::move appeared as an ownership-transfer tool. This lesson explains what rvalues are, […]

    Like

Leave a comment