Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Ownership, Templates, Errors and Concurrency

Smart Pointers and Ownership

Replace raw new and delete with std::unique_ptr and std::shared_ptr, break reference cycles with std::weak_ptr, and learn the ownership rules that prevent leaks.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A small organization chart. A manager owns their reports, each report points back to the manager without creating a cycle, and a tracing class shows exactly when every object is created and destroyed.

The idea

Every object needs one clear owner responsible for destroying it. Raw pointers (T*) don't say who owns what, so modern C++ expresses ownership in the type:

  • std::unique_ptr<T>: sole ownership. It cannot be copied, only moved. The object is destroyed when the pointer goes out of scope. This is the default choice; it costs nothing compared to a raw pointer. Create with std::make_unique<T>(args...).
  • std::shared_ptr<T>: shared ownership with a reference count; the object is destroyed when the last owner goes. Create with std::make_shared<T>(args...). Use it sparingly, only when ownership is genuinely shared.
  • std::weak_ptr<T>: a non-owning observer of a shared_ptr. Call lock() to get a shared_ptr if the object is still alive, or an empty one if it isn't. Use it to break cycles: two objects holding shared_ptrs to each other never reach zero and leak.
  • Raw pointers and references are for non-owning access: "use this, but don't free it".

Rules of thumb: don't write new or delete in application code; return unique_ptr from factory functions; pass T& or T* to functions that merely use an object.

The code

unique_ptr, shared_ptr and weak_ptr with a tracing class
#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Trace {
    std::string name;
    explicit Trace(std::string n) : name{std::move(n)} { std::cout << "  create " << name << '\n'; }
    ~Trace() { std::cout << "  destroy " << name << '\n'; }
};

struct Employee {
    Trace trace;
    std::weak_ptr<Employee> manager;                    // non-owning link back (no cycle)
    std::vector<std::shared_ptr<Employee>> reports;     // owning links down

    explicit Employee(std::string name) : trace{std::move(name)} {}
};

std::unique_ptr<Trace> make_trace(const std::string& name) {   // a factory returns ownership
    return std::make_unique<Trace>(name);
}

int main() {
    std::cout << "unique_ptr:\n";
    {
        auto a = make_trace("report.pdf");
        auto b = std::move(a);                          // ownership moves; a is now empty
        std::cout << "  a is " << (a ? "set" : "empty") << ", b is " << (b ? "set" : "empty") << '\n';
    }                                                   // b's Trace is destroyed here

    std::cout << "shared_ptr and weak_ptr:\n";
    {
        auto boss = std::make_shared<Employee>("Boss");
        auto dev = std::make_shared<Employee>("Dev");
        dev->manager = boss;                            // weak: does not keep boss alive
        boss->reports.push_back(dev);

        std::cout << "  boss use_count = " << boss.use_count() << '\n';    // 1
        std::cout << "  dev use_count  = " << dev.use_count() << '\n';     // 2: dev + boss->reports

        if (auto m = dev->manager.lock()) {
            std::cout << "  dev's manager is " << m->trace.name << '\n';
        }
    }   // both released: no cycle, so both destructors run
    std::cout << "done\n";
}

Output

unique_ptr:
  create report.pdf
  a is empty, b is set
  destroy report.pdf
shared_ptr and weak_ptr:
  create Boss
  create Dev
  boss use_count = 1
  dev use_count  = 2
  dev's manager is Boss
  destroy Dev
  destroy Boss
done

Run this on your computer — see “Get Started” for setup.

Change dev->manager to a shared_ptr<Employee> and the two objects keep each other alive forever: the "destroy" lines never appear.

Common mistakes

  • Using new and delete directly. An early return or exception between them leaks memory. Use make_unique.
  • Reaching for shared_ptr by default. It adds overhead and blurs ownership. Start with unique_ptr.
  • Creating shared_ptr cycles. Parent-to-child owns; child-to-parent observes (weak_ptr or a raw pointer).
  • Making two shared_ptrs from the same raw pointer. Each has its own count, so the object is deleted twice. Create one with make_shared and copy it.
  • Passing smart pointers to functions that only use the object. Pass T& or const T& instead; accept a smart pointer only when taking or sharing ownership.

Key points

  • Express ownership in types: unique_ptr for sole ownership, shared_ptr for shared, weak_ptr to observe.
  • Create with make_unique and make_shared; avoid new and delete.
  • Break ownership cycles with weak_ptr (or a non-owning raw pointer).
  • A raw T* or T& means "borrow, don't own".

Further reading: cppreference: Dynamic memory management.

Practice questions

When would you choose `unique_ptr` over `shared_ptr`?

Answer

Almost always. unique_ptr models sole ownership with no overhead and a clear lifetime. Use shared_ptr only when several owners genuinely need to keep an object alive and none can be designated the single owner.

How does `weak_ptr` solve circular references?

Answer

A weak_ptr observes an object managed by shared_ptr without increasing the reference count. If a child refers to its parent through a weak_ptr, the parent's count can reach zero, so both objects are destroyed.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?