Skip to content

← References, Pointers, Ownership step 3 of 4

Medium Primitives

The gap between new and the smart pointer

Sometimes an object genuinely must outlive the scope that made it, or must be referred to through a base class. Then it goes on the heap, and the question is who deletes it.

std::unique_ptr<T> is the answer unless you can prove you need another one. Exactly one owner, destroyed when that owner goes out of scope.

It is not a compromise you accept for safety. A unique_ptr is the size of a pointer, and its operator-> compiles to the same instruction a raw pointer does. There is no reference count, no allocation, no indirection. You are giving up nothing.

What it gives you is a signature that cannot be misread:

std::unique_ptr<Shape> make_shape(...);   // caller now owns this
void consume(std::unique_ptr<Shape> s);   // callee takes it
void inspect(const Shape& s);             // borrowed, definitely there
Shape* find(...);                         // borrowed, maybe not there

Four different intentions, four different types, none of them a comment.

Use make_unique. Always.

auto a = std::unique_ptr<Rect>(new Rect(w, h));   // don't
auto b = std::make_unique<Rect>(w, h);            // do

It is shorter, it says the type once instead of twice, and — the reason this problem exists — there is no moment where the object is owned by nobody.

In the first line, new Rect(w, h) returns a raw pointer, and then the unique_ptr is constructed from it. Between those two events the object is live and unowned. Anything that goes wrong in that window leaks it: an exception thrown while evaluating another argument, an early return added by someone six months later, or simply a branch that decides the object was not wanted after all.

That last one is not exotic. It is the most common leak in code that has already adopted smart pointers, because the author thinks the smart pointer is protecting them and the raw pointer above it is not covered yet.

The rest of the interface, briefly

p.get() the raw pointer, as a non-owning observer — never delete it
p.reset() destroy what is held, hold nothing
p.release() give up ownership and return the raw pointer — you now owe a delete
std::move(p) transfer ownership; p is guaranteed null afterwards

release() is the one to be suspicious of. It exists for handing a pointer to a C API, and almost every other use is a leak with extra steps.

Your task

std::unique_ptr<Shape> make_shape(int kind, int a, int b);

Kind 1 is a Rect(a, b), kind 2 is a Square(a), and anything else is nullptr. A shape whose area is zero or negative is also rejected: return nullptr rather than the shape.

audit is given and calls it; do not change it. Shape counts how many instances are alive, and the harness reads that count after audit has returned and every kept shape has been destroyed. It must be zero.

The starter allocates first and decides second.