Smart Pointers

Choosing the Correct Smart Pointer

The proper smart pointer is the one that reflects the ownership model that's really necessary for the program. In most situations, that will be a single ownership model, and so the default choice will be std::unique_ptr. A std::shared_ptr should only be used when multiple owners are expected to keep an object alive collectively; a std::weak_ptr should be used when code needs to observe an object that's already owned by one or more shared_ptrs.

If an object has a single obvious owner that is responsible for deleting it, std::unique_ptr is the correct choice. It is the smallest and fastest of the smart pointers, and it encodes sole ownership of an object right into its type. For instance, a function that creates an object and gives ownership of it to the caller:

#include <iostream>
#include <memory>

class Tracer
{
public:
    Tracer()
    {
        std::cout << "Tracer constructed\n";
    }

    // Run this destructor when the object lifetime ends
    ~Tracer()
    {
        std::cout << "Tracer destroyed\n";
    }
};

std::unique_ptr<Tracer> createTracer()
{
    return std::make_unique<Tracer>();
}

int main()
{
    std::unique_ptr<Tracer> p{ createTracer() };
    std::cout << "main owns the object\n";
    return 0;
}

createTracer returns a unique_ptr by value, but simply transfers ownership to the caller. The object then exists as long as p exists, and is destructed automatically when p goes out of scope. No other part of the program can accidentally delete it or keep it alive any longer.

Shared ownership should be used if the entire life of an object depends on multiple independent parts of a program. Then, for sure, std::shared_ptr will be a good choice for the implementation. Each instance of the shared_ptr will own one instance of an object, and the object will only be destroyed when the last shared_ptr refers to it. For example, you might have two variables that both need access to the same object.

#include <iostream>
#include <memory>

class Tracer
{
public:
    Tracer()
    {
        std::cout << "Tracer constructed\n";
    }

    ~Tracer()
    {
        std::cout << "Tracer destroyed\n";
    }
};

int main()
{
    // Create the object with shared ownership
    std::shared_ptr<Tracer> p1{ std::make_shared<Tracer>() };
    std::shared_ptr<Tracer> p2{ p1 };

    std::cout << "use count is " << p1.use_count() << '\n';
    return 0;
}

Both p1 and p2 own the same Tracer. The reference count is 2. When main ends, this drops back down to zero and the object is destroyed just once. Using a shared_ptr for what only needs unique ownership would increase the reference count traffic unnecessarily and cloud the ownership graph.

A std::weak_ptr never owns the object. It is used to break reference cycles or track an object whose lifetime is managed by shared_ptrs elsewhere. The classic cycle is two objects holding a shared_ptr to each other; the reference counts never go to zero and the objects leak. If you replace one of the links with a weak_ptr you get:

#include <iostream>
#include <memory>

class B;

class A
{
public:
    std::shared_ptr<B> b_ptr;

    // Run this destructor when the object lifetime ends
    ~A()
    {
        std::cout << "A destroyed\n";
    }
};

class B
{
public:
    std::weak_ptr<A> a_ptr;

    ~B()
    {
        std::cout << "B destroyed\n";
    }
};

int main()
{
    std::shared_ptr<A> a{ std::make_shared<A>() };
    std::shared_ptr<B> b{ std::make_shared<B>() };

    a->b_ptr = b;
    b->a_ptr = a;

    return 0;
}

Since B only has a weak_ptr to A, it does not keep it alive. Once main exits, both objects are destroyed in the proper order and the following output is displayed: "A destroyed", "B destroyed".

The simple practical tip that emerges from this discussion is quite straightforward. Use std::unique_ptr and std::make_unique whenever possible. Use std::shared_ptr and std::make_shared only when ownership needs to be shared.

Use a std::weak_ptr only when an observer that does not own the object is needed or when a circular reference needs to be broken.

Exercise: Choosing the Correct Smart PointerStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Choosing the Correct Smart Pointer

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Explicitly reset the unique owner returned by the factory and print a message afterward. Explain why shared ownership is unnecessary for this lifetime.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>
#include <memory>

class Tracer
{
public:
    Tracer()
    {
        std::cout << "Tracer constructed\n";
    }

    // Run this destructor when the object lifetime ends
    ~Tracer()
    {
        std::cout << "Tracer destroyed\n";
    }
};

std::unique_ptr<Tracer> createTracer()
{
    return std::make_unique<Tracer>();
}

int main()
{
    std::unique_ptr<Tracer> p{ createTracer() };
    std::cout << "main owns the object\n";
    return 0;
}

Hints

Hint 1

createTracer returns one unique_ptr to main, expressing a single owner. Returning that smart pointer does not require adding shared ownership.

Hint 2

Identify whether any other part of this example needs to keep the Tracer alive independently. With only main owning it, unique_ptr communicates the lifetime more directly than shared_ptr.

Worked solution

The factory result has a single owner in main. Explicitly resetting that owner shows the destruction point before the next message; no independent owner requires shared_ptr.

#include <iostream>
#include <memory>

class Tracer
{
public:
    Tracer()
    {
        std::cout << "Tracer constructed\n";
    }

    // Run this destructor when the object lifetime ends
    ~Tracer()
    {
        std::cout << "Tracer destroyed\n";
    }
};

std::unique_ptr<Tracer> createTracer()
{
    return std::make_unique<Tracer>();
}

int main()
{
    std::unique_ptr<Tracer> p{ createTracer() };
    std::cout << "main owns the object\n";
    p.reset();
    std::cout << "Factory object released\n";
    return 0;
}

Example output

Tracer constructed
main owns the object
Tracer destroyed
Factory object released

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.