Docs

README

Smart Pointers in C++

Table of Contents

  1. Why Smart Pointers
  2. unique_ptr
  3. shared_ptr
  4. weak_ptr
  5. Custom Deleters
  6. Best Practices

Why Smart Pointers

The Problem with Raw Pointers

void riskyFunction() {
    int* data = new int[1000];

    // What if processData() throws?
    processData(data);

    // What if we return early?
    if (someCondition) {
        return;  // MEMORY LEAK!
    }

    delete[] data;  // Might never reach here
}

The Solution: RAII with Smart Pointers

Smart pointers automatically manage memory through RAII:

  • Acquire resource in constructor
  • Release resource in destructor
  • No manual delete needed
#include <memory>

void safeFunction() {
    auto data = make_unique<int[]>(1000);

    processData(data.get());  // Even if throws, destructor runs

    if (someCondition) {
        return;  // No leak! unique_ptr destructor frees memory
    }
}  // Automatically deleted here

Three Smart Pointer Types

TypeOwnershipUse Case
unique_ptrExclusiveSingle owner, most common
shared_ptrSharedMultiple owners
weak_ptrNon-owningBreaking cycles, caching

unique_ptr

The most common and efficient smart pointer.

Basic Usage

#include <memory>

// Create unique_ptr
unique_ptr<int> p1 = make_unique<int>(42);     // Preferred
unique_ptr<int> p2(new int(42));               // Direct construction

// Access the value
cout << *p1 << endl;      // Dereference
cout << p1.get() << endl; // Get raw pointer

// Reset to new value
p1.reset(new int(100));   // Delete old, point to new
p1.reset();               // Delete and become nullptr

// Check if pointing to something
if (p1) {
    cout << "p1 is valid" << endl;
}

Arrays with unique_ptr

// Array specialization
unique_ptr<int[]> arr = make_unique<int[]>(10);
arr[0] = 42;  // Bracket access

// Automatically calls delete[]

Move Semantics (No Copying!)

unique_ptr<int> p1 = make_unique<int>(42);

// CANNOT copy
// unique_ptr<int> p2 = p1;  // ERROR!

// CAN move
unique_ptr<int> p2 = move(p1);  // p1 is now nullptr

Common Operations

unique_ptr<int> ptr = make_unique<int>(42);

ptr.get();        // Raw pointer (don't delete!)
*ptr;             // Dereference
ptr.reset();      // Delete and set to nullptr
ptr.reset(new_p); // Delete and point to new
ptr.release();    // Release ownership, return raw ptr
ptr = nullptr;    // Same as reset()
if (ptr) { }      // Check if valid

Transfer Ownership

unique_ptr<Widget> createWidget() {
    return make_unique<Widget>();  // Move on return
}

void takeOwnership(unique_ptr<Widget> w) {
    // Widget deleted when function ends
}

unique_ptr<Widget> w = createWidget();
takeOwnership(move(w));  // w is now nullptr

shared_ptr

For shared ownership - multiple pointers can own the same resource.

Basic Usage

#include <memory>

// Create shared_ptr
shared_ptr<int> sp1 = make_shared<int>(42);  // Preferred
shared_ptr<int> sp2(new int(42));            // Direct

// Copy is allowed! (increases reference count)
shared_ptr<int> sp3 = sp1;
shared_ptr<int> sp4 = sp1;

cout << sp1.use_count() << endl;  // 3 (sp1, sp3, sp4)

Reference Counting

{
    shared_ptr<int> p1 = make_shared<int>(42);  // count = 1
    cout << "Count: " << p1.use_count() << endl;

    {
        shared_ptr<int> p2 = p1;  // count = 2
        shared_ptr<int> p3 = p1;  // count = 3
        cout << "Count: " << p1.use_count() << endl;
    }  // p2, p3 destroyed, count = 1

    cout << "Count: " << p1.use_count() << endl;
}  // p1 destroyed, count = 0, memory freed

Common Operations

shared_ptr<int> ptr = make_shared<int>(42);

ptr.get();        // Raw pointer
*ptr;             // Dereference
ptr.use_count();  // Number of owners
ptr.unique();     // true if use_count() == 1
ptr.reset();      // Decrease count, maybe delete
ptr = nullptr;    // Same as reset()
if (ptr) { }      // Check if valid

Arrays with shared_ptr (C++17+)

// C++17 and later
shared_ptr<int[]> arr = make_shared<int[]>(10);
arr[0] = 42;

// Pre-C++17: use custom deleter
shared_ptr<int> arr(new int[10], default_delete<int[]>());

make_shared vs new

// Preferred: make_shared
auto p1 = make_shared<Widget>(args);

// Avoid: new
shared_ptr<Widget> p2(new Widget(args));

Why prefer make_shared:

  1. One memory allocation (instead of two)
  2. Exception safe
  3. More efficient

weak_ptr

Non-owning observer of shared_ptr. Doesn't affect reference count.

Purpose

  1. Break circular references
  2. Caching - observe without keeping alive
  3. Observer pattern - safely check if object still exists

Basic Usage

shared_ptr<int> sp = make_shared<int>(42);
weak_ptr<int> wp = sp;  // Doesn't increase count

cout << sp.use_count() << endl;  // Still 1

// Must convert to shared_ptr to use
if (auto locked = wp.lock()) {
    cout << *locked << endl;  // Safe to use
} else {
    cout << "Object destroyed" << endl;
}

Detecting Expiration

shared_ptr<int> sp = make_shared<int>(42);
weak_ptr<int> wp = sp;

cout << wp.expired() << endl;  // false

sp.reset();  // Object destroyed

cout << wp.expired() << endl;  // true

Breaking Circular References

Without weak_ptr (memory leak):

struct Node {
    shared_ptr<Node> next;  // Circular!
    shared_ptr<Node> prev;  // Memory leak
};

With weak_ptr (correct):

struct Node {
    shared_ptr<Node> next;
    weak_ptr<Node> prev;  // Won't keep alive
};

Example: Observer Pattern

class Subject {
    vector<weak_ptr<Observer>> observers;

public:
    void notify() {
        for (auto& wp : observers) {
            if (auto sp = wp.lock()) {
                sp->update();
            }
        }
    }
};

Custom Deleters

Customize how resources are freed.

With unique_ptr

// Lambda deleter
auto deleter = [](int* p) {
    cout << "Custom delete" << endl;
    delete p;
};
unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);

// Function pointer deleter
void myDeleter(FILE* f) {
    if (f) fclose(f);
}
unique_ptr<FILE, decltype(&myDeleter)> file(fopen("test.txt", "r"), myDeleter);

With shared_ptr

// Easier - deleter is type-erased
shared_ptr<int> ptr(new int(42), [](int* p) {
    cout << "Custom delete" << endl;
    delete p;
});

// File example
shared_ptr<FILE> file(fopen("test.txt", "r"), fclose);

Practical Examples

// Managing C resources
shared_ptr<SDL_Window> window(
    SDL_CreateWindow(...),
    SDL_DestroyWindow
);

// Array with shared_ptr (pre-C++17)
shared_ptr<int> arr(new int[10], default_delete<int[]>());

// Custom pool allocator
auto poolDeleter = [&pool](Widget* w) { pool.deallocate(w); };
unique_ptr<Widget, decltype(poolDeleter)> w(pool.allocate(), poolDeleter);

Best Practices

✅ Do

// 1. Use make_unique/make_shared
auto p1 = make_unique<Widget>();
auto p2 = make_shared<Widget>();

// 2. Pass by value to transfer ownership
void takeOwnership(unique_ptr<Widget> w);

// 3. Pass by reference to use without ownership
void useWidget(const Widget& w);
void useWidget(Widget* w);  // If nullable

// 4. Return unique_ptr from factories
unique_ptr<Widget> createWidget() {
    return make_unique<Widget>();
}

// 5. Use weak_ptr for breaking cycles
struct Node {
    shared_ptr<Node> next;
    weak_ptr<Node> prev;
};

// 6. Check weak_ptr before use
if (auto sp = wp.lock()) {
    // safe to use sp
}

❌ Don't

// 1. Don't use raw new with smart pointers (exception safety)
shared_ptr<Widget> p(new Widget());  // OK but not ideal
auto p = make_shared<Widget>();      // Better

// 2. Don't call delete on smart pointer's raw pointer
Widget* raw = ptr.get();
delete raw;  // WRONG! Double delete

// 3. Don't create multiple smart pointers from raw pointer
Widget* raw = new Widget();
shared_ptr<Widget> p1(raw);
shared_ptr<Widget> p2(raw);  // WRONG! Double delete

// 4. Don't use shared_ptr when unique_ptr suffices
// unique_ptr is more efficient

// 5. Don't return reference to local smart pointer's content
Widget& createWidget() {
    auto p = make_unique<Widget>();
    return *p;  // DANGLING!
}

Guidelines Summary

  1. Prefer unique_ptr - most efficient, clearest ownership
  2. Use shared_ptr only when truly needed
  3. Use weak_ptr to break cycles
  4. Always use make_unique/make_shared
  5. Pass unique_ptr by value to transfer ownership
  6. Pass by reference/pointer when not taking ownership
  7. Return unique_ptr from factory functions

Comparison

FeatureRaw Pointerunique_ptrshared_ptr
OwnershipNoneExclusiveShared
CopyableYesNoYes
MovableYesYesYes
OverheadNoneNoneReference count
Thread-safeNoNoCount is atomic
Use caseNon-owningSingle ownerMultiple owners

Quick Reference

#include <memory>

// unique_ptr
unique_ptr<T> p = make_unique<T>(args);
unique_ptr<T[]> arr = make_unique<T[]>(size);
p.get();              // Raw pointer
p.reset();            // Delete and nullify
p.release();          // Release ownership
unique_ptr<T> p2 = move(p);  // Transfer ownership

// shared_ptr
shared_ptr<T> p = make_shared<T>(args);
p.use_count();        // Reference count
p.unique();           // true if count == 1
shared_ptr<T> p2 = p; // Copy (increments count)

// weak_ptr
weak_ptr<T> wp = sp;
wp.expired();         // True if object gone
wp.lock();            // Get shared_ptr (or nullptr)

// Common patterns
auto p = make_unique<Widget>();       // Factory
takeOwnership(move(p));               // Transfer
useWidget(*p);                        // Use by ref

Compile & Run

g++ -std=c++17 -Wall -Wextra examples.cpp -o examples
./examples
README - C++ Tutorial | DeepML