C++ / MEMORY OWNERSHIP AND SMART POINTERS
unique_ptr for single ownership
Use std::unique_ptr to give a heap object exactly one owner, move that ownership between scopes, and write signatures that state who deletes.
What you will learn
- Create owners with std::make_unique and let the destructor issue every delete
- Move ownership with std::move and treat the moved-from unique_ptr as null
- Read and write signatures that say who owns: by value, by reference, or raw pointer
- Use get to borrow, reset to destroy, release to hand the delete to someone else
Understanding unique_ptr for single ownership
std::unique_ptr<T> stores one raw pointer and calls delete on it in its destructor, but the part that makes it worth using is what it refuses to do: its copy constructor and copy assignment operator are deleted. That means the compiler, not a runtime check, enforces the invariant that at most one unique_ptr names a given object, because code that would create a second owner simply does not build. With the default deleter it is normally the same size as a raw pointer and compiles to the same work as a hand-written delete, so keeping a heap object in a bare pointer buys you nothing.
Ownership still has to move around, and std::move is how you say so. Moving a unique_ptr is a pointer steal: the destination takes the address and the source is left holding nullptr. The standard pins that state down exactly, so a moved-from unique_ptr is guaranteed to be null and you may test it or assign a new object to it, while dereferencing it is undefined behaviour. Since the copy is deleted, every transfer must be written out with std::move, which turns each ownership change into something visible in the source.
That turns the type into a contract that appears in signatures. A unique_ptr<T> parameter taken by value means the function takes ownership; a T& or const T& parameter means it only borrows and the caller remains the owner; returning unique_ptr<T> by value is how a factory hands a fresh object to its caller. The three accessors split the same way: get lends out the address without giving anything up, reset destroys whatever you currently hold, and release abandons ownership without destroying anything.
<iostream>
<memory>
<string>
<utility>
struct Connection {
std::string name;
explicit Connection(std::string n) : name(std::move(n)) {
std::cout << "open " << name << "\n";
}
~Connection() { std::cout << "close " << name << "\n"; }
void ping() const { std::cout << "ping " << name << "\n"; }
};
std::unique_ptr<Connection> makeConnection(const char* n) {
return std::make_unique<Connection>(n); // ownership travels out by move
}
void observe(const Connection& c) { c.ping(); } // borrows, never deletes
int main() {
std::unique_ptr<Connection> a = makeConnection("db");
observe(*a);
// std::unique_ptr<Connection> copy = a; // will not compile: copy is deleted
std::unique_ptr<Connection> b = std::move(a);
std::cout << "after move, a is " << (a ? "owning" : "null") << "\n";
observe(*b);
b.reset();
std::cout << "after reset, b is " << (b ? "owning" : "null") << "\n";
std::cout << "main done\n";
}
unique_ptr encodes single ownership in the type system: it cannot be copied, only moved, so exactly one handle is ever responsible for the delete.
Worked examples
A vector of owners
Stores polymorphic objects in a container of unique_ptr and shows when each one is destroyed.
<iostream>
<memory>
<vector>
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) {}
~Square() override { std::cout << "Square gone\n"; }
double area() const override { return side * side; }
};
struct Rect : Shape {
double w, h;
Rect(double w, double h) : w(w), h(h) {}
~Rect() override { std::cout << "Rect gone\n"; }
double area() const override { return w * h; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Square>(3.0));
shapes.push_back(std::make_unique<Rect>(2.0, 5.0));
double total = 0.0;
for (const auto& s : shapes) total += s->area();
std::cout << "count " << shapes.size() << " total " << total << "\n";
shapes.erase(shapes.begin());
std::cout << "count " << shapes.size()
<< " front area " << shapes.front()->area() << "\n";
}
Example explained
Line 1push_back accepts the make_unique result because it is a temporary, so the vector move-constructs the element and no copy is required.
Line 2The loop must use const auto&; writing auto s would attempt to copy a unique_ptr and fail to compile.
Line 3erase move-assigns the Rect element over element 0, and that assignment deletes the Square the slot was holding, which is why Square gone prints before the next output line.
Line 4virtual ~Shape() is mandatory: the default deleter calls delete on a Shape*, so without it the Square and Rect destructors would be skipped.
get, release, and reset
Distinguishes the three accessors by showing exactly which of them deletes the owned object.
<iostream>
<memory>
struct Node {
int v;
explicit Node(int v) : v(v) {}
~Node() { std::cout << "destroy " << v << "\n"; }
};
void legacyAdopt(Node* n) { // old API that deletes what it is handed
std::cout << "legacy got " << n->v << "\n";
delete n;
}
int main() {
std::unique_ptr<Node> p = std::make_unique<Node>(1);
std::cout << "borrowed " << p.get()->v << "\n";
legacyAdopt(p.release());
std::cout << "p is " << (p ? "owning" : "null") << "\n";
p.reset(new Node(2));
p.reset(new Node(3));
std::cout << "p owns " << p->v << "\n";
}
Example explained
Line 1p.get() hands out the address while p keeps ownership, so it is only safe for code that neither stores nor deletes the pointer.
Line 2release() returns the raw pointer and nulls p without deleting; here legacyAdopt becomes the deleter, and skipping that step would leak Node 1.
Line 3The second reset destroys the Node holding 2 before adopting 3, so reset is the verb that deletes.
Line 4The final destroy line happens at the closing brace of main, when p's own destructor deletes Node 3.
A sink parameter
Uses a by-value unique_ptr parameter to express that a function takes ownership, and hands out a raw pointer for looking only.
<iostream>
<memory>
<vector>
struct Task {
int id;
explicit Task(int id) : id(id) {}
~Task() { std::cout << "task " << id << " destroyed\n"; }
};
class Queue {
public:
void submit(std::unique_ptr<Task> t) { // by value: hand it over
std::cout << "queued " << t->id << "\n";
items.push_back(std::move(t));
}
Task* peek() const { return items.empty() ? nullptr : items.front().get(); }
private:
std::vector<std::unique_ptr<Task>> items;
};
int main() {
std::cout << "unique_ptr size == raw size: " << std::boolalpha
<< (sizeof(std::unique_ptr<Task>) == sizeof(Task*)) << "\n";
Queue q;
auto t = std::make_unique<Task>(7);
q.submit(std::move(t));
std::cout << "caller now holds " << (t ? "a task" : "nothing") << "\n";
std::cout << "peek id " << q.peek()->id << "\n";
std::cout << "leaving main\n";
}
Example explained
Line 1The by-value unique_ptr parameter forces the caller to write std::move, so the transfer of ownership is visible at the call site.
Line 2Inside submit, t is a local owner and must be moved again into the vector; a plain push_back(t) would not compile.
Line 3peek deliberately returns a raw Task*: the queue keeps ownership and the caller is only permitted to look at the object.
Line 4The size test prints true on the usual implementations because std::default_delete is an empty stateless type that costs no storage; that is a quality-of-implementation property, not a standard guarantee.
Important notes
With the default deleter, unique_ptr<Base> calls delete on a Base*, so Base needs a virtual destructor or the derived destructor is skipped and the behaviour is undefined.
Arrays need the array specialisation: std::unique_ptr<T[]> and std::make_unique<T[]>(n) call delete[], while a unique_ptr<T> holding an array would call the wrong delete.
Common mistakes
Touching the moved-from pointer: after auto b = std::move(a), a holds nullptr, so a->field dereferences null and typically crashes.
Assuming release() destroys the object; it only drops ownership, so the object leaks unless something else deletes the returned pointer.
Building a second owner from p.get(), or from the same new-expression twice, which makes two destructors call delete on one address: a double free.
Try it yourself
Change, predict, then run
Write a Logger struct that prints its name in both the constructor and destructor, fill a std::vector<std::unique_ptr<Logger>> with three of them, then inside a nested scope move the middle element into a local unique_ptr. Predict the order of all six printed lines before running it.
Open the C++ workspaceCheck your understanding
A function is declared void consume(std::unique_ptr<Widget> w); and you call consume(std::move(p)); . Immediately after consume returns, what is true of p and the Widget?
- p still owns the Widget, since std::move only marks the argument as movable
- p is null and the Widget has already been destroyed, because the parameter owned it and died when consume returned
- p is null but the Widget is still alive, because the parameter was only a non-owning view
- The call fails to compile, because a unique_ptr cannot be passed by value
Show answer
The by-value parameter is a full owner: the move handed it the pointer and left p null, which the standard guarantees, and the parameter's destructor deleted the Widget at the end of consume. Option 0 is tempting because std::move is only a cast to an rvalue reference, but that cast is exactly what selects unique_ptr's move constructor, and that constructor really does null the source; a by-value unique_ptr parameter is never a mere view, which also rules out option 2.