Why Smart Pointers?
{:.gc-basic}
Basic
Raw pointers require manual delete — easy to forget on early returns, exceptions, or complex control flow. Smart pointers are RAII wrappers that automatically delete the managed object when it goes out of scope.
// Raw pointer — who deletes it? When?
void bad() {
int* p = new int(42);
if (some_condition) return; // LEAK — delete never called
delete p;
}
// Smart pointer — always cleaned up
void good() {
auto p = std::make_unique<int>(42);
if (some_condition) return; // OK — destructor runs, no leak
}
| Smart Pointer | Ownership | When to Use |
|---|---|---|
unique_ptr<T> |
Exclusive (one owner) | Default choice for heap objects |
shared_ptr<T> |
Shared (ref-counted) | Multiple owners needed |
weak_ptr<T> |
Non-owning observer | Break shared_ptr cycles |
std::unique_ptr
{:.gc-basic}
Basic
unique_ptr models exclusive ownership. It cannot be copied — only moved. Zero overhead over a raw pointer.
#include <memory>
// Preferred: make_unique (C++14)
auto sensor = std::make_unique<Sensor>(42, 23.5f);
// Access like a raw pointer
sensor->activate();
float v = sensor->read();
// unique_ptr is non-copyable, movable
auto s2 = std::move(sensor); // sensor is now null; s2 owns it
// sensor.get() == nullptr
// Release ownership (raw pointer — you own it now)
Sensor* raw = s2.release();
delete raw; // now you must delete it
// Reset to a new object (old object destroyed)
s2.reset(new Sensor(99, 0.0f));
s2.reset(); // destroys and sets to null
Factory Functions with unique_ptr
// Return unique_ptr from factory functions
std::unique_ptr<IDevice> create_device(const std::string& type) {
if (type == "uart") return std::make_unique<UartDevice>();
if (type == "spi") return std::make_unique<SpiDevice>();
return nullptr;
}
// Caller takes ownership
auto dev = create_device("uart");
if (dev) dev->open("/dev/ttyS0");
// dev destroyed when it goes out of scope
unique_ptr for Arrays
// unique_ptr manages array correctly (calls delete[])
auto buf = std::make_unique<uint8_t[]>(1024);
buf[0] = 0xFF;
// But prefer std::vector<uint8_t> for dynamic arrays!
Custom Deleter
// Custom deleter for C file handles
auto fp = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("/etc/hostname", "r"), &fclose);
if (fp) {
char line[128];
fgets(line, sizeof(line), fp.get());
std::puts(line);
} // fclose called automatically
// Useful for OS resources (file descriptors, sockets)
struct FdDeleter {
void operator()(int* fd) const {
if (*fd >= 0) close(*fd);
delete fd;
}
};
std::shared_ptr
{:.gc-mid}
Intermediate
shared_ptr uses reference counting. The managed object is destroyed when the last shared_ptr to it is destroyed.
// make_shared allocates object + control block in one allocation (efficient)
auto a = std::make_shared<Sensor>(1, 0.0f);
auto b = a; // copy: both a and b own the sensor
std::cout << a.use_count() << '\n'; // 2
{
auto c = a; // use_count = 3
} // c destroyed: use_count = 2
a.reset(); // a released: use_count = 1
// b still holds the sensor; it's alive
// b goes out of scope: use_count = 0 → Sensor destroyed
Shared Ownership Example
struct EventBus {
std::vector<std::shared_ptr<Sensor>> subscribers;
void subscribe(std::shared_ptr<Sensor> s) {
subscribers.push_back(s);
}
void broadcast() {
for (auto& s : subscribers)
s->activate();
}
};
auto s1 = std::make_shared<Sensor>(1, 0.0f);
EventBus bus;
bus.subscribe(s1); // bus and caller both own s1
// s1 is still alive as long as bus holds a copy
shared_ptr Performance Considerations
// make_shared: single allocation (object + control block)
auto good = std::make_shared<Sensor>(1, 0.0f); // ✓
// Direct constructor: two allocations
auto bad = std::shared_ptr<Sensor>(new Sensor(1, 0.0f)); // ✗
// Thread safety: ref-count operations are atomic, but the object itself is NOT
// Use mutex to protect shared object data when accessed from multiple threads
std::weak_ptr
{:.gc-mid}
weak_ptr holds a non-owning reference to a shared_ptr-managed object. It does not affect the reference count. Use it to break reference cycles and to observe objects without keeping them alive.
struct Node {
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> parent; // ← weak to break cycle
Node(int v) : value(v) {}
};
auto root = std::make_shared<Node>(1);
auto child = std::make_shared<Node>(2);
root->next = child; // root owns child (strong)
child->parent = root; // child observes root (weak — no cycle!)
// To use a weak_ptr, promote to shared_ptr (may return nullptr)
if (auto p = child->parent.lock()) {
std::cout << "parent value: " << p->value << '\n'; // 1
} else {
std::cout << "parent is gone\n";
}
Cycle Without weak_ptr — Memory Leak
struct BadNode {
std::shared_ptr<BadNode> next; // shared both ways = cycle = leak!
};
auto a = std::make_shared<BadNode>();
auto b = std::make_shared<BadNode>();
a->next = b;
b->next = a; // a and b keep each other alive forever
Ownership Guidelines
{:.gc-adv}
Advanced
1. Use unique_ptr by default.
2. Use shared_ptr only when ownership is genuinely shared.
3. Use weak_ptr to break shared_ptr cycles.
4. Pass by raw pointer or reference when NOT transferring ownership.
5. Never mix raw new/delete with smart pointers for the same object.
// Function signatures that express ownership clearly:
// Takes ownership (caller must pass owned pointer)
void take_device(std::unique_ptr<IDevice> dev);
// Borrows (caller keeps ownership, function doesn't store it)
void use_device(IDevice& dev);
void use_device(IDevice* dev); // nullable borrow
// Shares ownership (function may outlive the call)
void register_device(std::shared_ptr<IDevice> dev);
// Observes without owning
void observe(std::weak_ptr<IDevice> dev);
enable_shared_from_this
// When an object needs to create a shared_ptr to itself:
class Server : public std::enable_shared_from_this<Server> {
public:
void start_session() {
auto self = shared_from_this(); // safe — returns shared_ptr<Server>
// pass self to async callback; keeps Server alive
}
};
// Only valid when Server is already managed by a shared_ptr:
auto srv = std::make_shared<Server>();
srv->start_session(); // OK
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — Basic: When do you use unique_ptr vs shared_ptr?
Use
unique_ptrwhen exactly one owner holds the resource — it has zero overhead and expresses exclusive ownership clearly. Useshared_ptronly when multiple independent owners genuinely need to share the resource and the last owner should destroy it. Shared ownership is a design smell in many cases — prefer restructuring code to use unique ownership. A good heuristic: start withunique_ptr; only promote toshared_ptrwhen you cannot avoid it.
Q2 — Basic: What does make_unique and make_shared do differently from new?
make_unique<T>(args)constructs T withnew, wraps it inunique_ptr<T>, and (critically) is exception-safe — if the constructor throws after the allocation, the pointer is correctly destroyed.make_shared<T>(args)additionally allocates the T object and its reference-count control block in a single allocation, which is more cache-friendly and efficient thanshared_ptr<T>(new T(args))which requires two allocations.
Q3 — Intermediate: What is a weak_ptr cycle and how does weak_ptr fix it?
When two objects hold
shared_ptrto each other, their reference counts never reach zero — a memory leak.weak_ptrholds a reference to the object without incrementing its reference count. The object can be destroyed whileweak_ptrremains; callinglock()returns ashared_ptrornullptrif the object is gone. By replacing one of theshared_ptrmembers withweak_ptr(e.g. parent→child is strong, child→parent is weak), the cycle is broken and normal destruction occurs.
Q4 — Advanced: Is shared_ptr thread-safe?
The reference-count operations (copy, destroy) are thread-safe — they use atomic increments/decrements. However, the managed object itself is not protected by
shared_ptr. Two threads accessing the same object through differentshared_ptrcopies must synchronise access to the object’s data themselves (mutex, atomics, etc.). Also, concurrent read-and-write access to the sameshared_ptrvariable (not the managed object) is not safe — you need a mutex oratomic<shared_ptr>(C++20).
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| cppreference — unique_ptr | en.cppreference.com/w/cpp/memory/unique_ptr |
| cppreference — shared_ptr | en.cppreference.com/w/cpp/memory/shared_ptr |
| C++ Core Guidelines — Resource management | isocpp.github.io/CppCoreGuidelines#S-resource |
| Herb Sutter — GotW #89: Smart Pointers | herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers |