Skip to content

Step 1 of 7 · Reading · ~4 min

Learn

Pointers and Memory

Smart Pointers: unique_ptr, shared_ptr and weak_ptr

You will pick the right owner type for an object, hand ownership between functions, and observe a shared object without keeping it alive. Last lesson every new needed a hand-written delete and you met four ways that fails; here the compiler writes the delete.

RAII: the scope owns the object

A smart pointer is a small object whose destructor does the delete. Destructors run when a scope ends, on every path out (early return, exception), so cleanup cannot be forgotten. Tying a resource to an object's lifetime is called RAII.

struct Sensor {
    std::string id;
    explicit Sensor(std::string i) : id(i) { std::cout << "on " << id << "\n"; }
    ~Sensor() { std::cout << "off " << id << "\n"; }
};
void probe() {
    auto s = std::make_unique<Sensor>("t1");
    std::cout << "reading " << s->id << "\n";
}   // s dies here, taking the Sensor with it

probe() prints on t1, reading t1, off t1.

unique_ptr: exactly one owner

std::unique_ptr<T> cannot be copied, so two owners are impossible by construction:

auto a = std::make_unique<Sensor>("t2");
auto b = a;             // does not compile

Clang says call to deleted constructor of 'std::unique_ptr<...>'. To hand the object over, spell it std::move:

auto b = std::move(a);
std::cout << (a ? "a owns" : "a empty");   // a empty

std::move(a) hands the pointer over and leaves a null (the move-semantics lesson explains how).

  • b.get() returns the raw pointer to borrow; b still owns it. b.reset() deletes now and leaves b null.
  • b.release() gives up ownership without deleting; the caller must delete the result.
  • std::make_unique<int[]>(4) owns four zeroed ints and calls delete[] for you.

The signature tells the reader who owns what:

void show(const Sensor& s);                  // borrows
void inspect(const Sensor* s);               // borrows; may be null
void adopt(std::unique_ptr<Sensor> s);       // takes ownership: adopt(std::move(p))
std::unique_ptr<Sensor> make(std::string);   // hands ownership to the caller

A vector of unique_ptr owns a mixed family; the base needs the virtual destructor you already have:

struct Shape { virtual ~Shape() = default; virtual int area() const = 0; };
struct Sq : Shape { int s; Sq(int s) : s(s) {} int area() const override { return s * s; } };
struct Rect : Shape { int w, h; Rect(int w, int h) : w(w), h(h) {} int area() const override { return w * h; } };

std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Sq>(2));
shapes.push_back(std::make_unique<Rect>(3, 5));
for (const auto& p : shapes) std::cout << p->area() << "\n";   // 4 then 15

Writing auto p in that loop copies each unique_ptr and fails to compile.

shared_ptr: counted owners

When several owners are genuinely needed, std::shared_ptr<T> keeps a count in a control block (make_shared allocates it together with the object). Each copy adds one, each destruction removes one, and the object dies at zero.

auto x = std::make_shared<Sensor>("t3");   // on t3
auto y = x;
std::cout << x.use_count();                // 2
x.reset();                                 // count 1, nothing printed
y.reset();                                 // count 0: off t3

A returned unique_ptr converts to shared ownership: std::shared_ptr<Sensor> s = make("t4");. The reverse is impossible.

weak_ptr: observe without owning

std::weak_ptr<T> watches a shared object but does not count. expired() says whether the object is gone, and lock() returns a shared_ptr that is empty if so.

auto z = std::make_shared<Sensor>("t5");
std::weak_ptr<Sensor> w = z;   // z owns; count stays 1
auto g = w.lock();             // g is a second owner while it lives
std::cout << z.use_count();    // 2, not 1

Weak pointers break cycles. Two Peers holding shared_ptr<Peer> to each other keep each other alive: leaving the scope prints nothing and both leak. Keep the forward link shared_ptr<Peer> next, make the back link weak_ptr<Peer> prev, and the counts are 1 and 2; scope exit prints free p then free q.

Common mistakes

  • Two owners from one raw pointer: std::shared_ptr<Sensor> a(raw), b(raw); makes two control blocks (both counts read 1), and the second delete aborts with double free detected. Copy the shared_ptr instead.
  • Counting the lock: w.lock().use_count() reads one too many, because the pointer you just locked is itself an owner.
  • Using a moved-from or released unique_ptr: it is null, so -> crashes.

Your exercise

A script of commands opens, shares, drops and observes named resources whose constructor and destructor print, so lifetimes show in the output. Make close print exactly when the last owner lets go. The likeliest mistake is testing a name with handles[name], which silently inserts an empty entry.

Up nextThe Rule of Five and RAII: Copying an OwnerModern C++ Patterns

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…