skip to content
Mehdi Mehdikhani
Table of Contents

Smart pointers in C++11 finally brought automatic memory management to C++. They’re one of the biggest improvements for writing safe, leak-free code.

What Smart Pointers Actually Are

Smart pointers are objects that wrap raw pointers and automatically manage their memory:

#include <memory>
// Old way - manual memory management
int* raw_ptr = new int(42);
// ... use the pointer
delete raw_ptr; // Easy to forget!
// New way - automatic cleanup
std::unique_ptr<int> smart_ptr = std::make_unique<int>(42);
// ... use the pointer
// Automatically deleted when smart_ptr goes out of scope

No more delete statements, no more memory leaks from forgotten cleanup.

The Three Main Types

C++11 provides three main smart pointer types:

// unique_ptr - exclusive ownership
std::unique_ptr<MyClass> unique = std::make_unique<MyClass>();
// shared_ptr - shared ownership with reference counting
std::shared_ptr<MyClass> shared = std::make_shared<MyClass>();
// weak_ptr - non-owning observer to break circular references
std::weak_ptr<MyClass> weak = shared;

Each serves a different purpose for different ownership semantics.

When I Actually Use Smart Pointers

Most of the time, I use smart pointers for:

  1. Dynamic object creation: Instead of raw new/delete:
class ResourceManager {
std::unique_ptr<Database> db_;
std::unique_ptr<Logger> logger_;
public:
ResourceManager()
: db_(std::make_unique<Database>("connection_string")),
logger_(std::make_unique<FileLogger>("app.log")) {
}
// Destructors automatically called when ResourceManager is destroyed
};
  1. Factory functions: Returning owned objects:
std::unique_ptr<Shape> create_shape(ShapeType type) {
switch (type) {
case ShapeType::Circle:
return std::make_unique<Circle>(5.0);
case ShapeType::Rectangle:
return std::make_unique<Rectangle>(10.0, 20.0);
default:
return nullptr; // unique_ptr can be null
}
}
auto shape = create_shape(ShapeType::Circle);
if (shape) {
shape->draw();
}
  1. Shared resources: When multiple objects need the same resource:
class AudioSystem {
std::shared_ptr<AudioDevice> device_;
public:
AudioSystem(std::shared_ptr<AudioDevice> device) : device_(device) {}
void play_sound(const std::string& filename) {
if (device_) {
device_->play(filename);
}
}
};
// Multiple systems can share the same device
auto audio_device = std::make_shared<AudioDevice>();
AudioSystem music_system(audio_device);
AudioSystem sfx_system(audio_device); // Shares the same device
  1. Breaking circular references: Using weak_ptr to avoid cycles:
class Parent {
std::vector<std::shared_ptr<Child>> children_;
public:
void add_child(std::shared_ptr<Child> child) {
children_.push_back(child);
child->set_parent(shared_from_this()); // Set parent as weak_ptr in child
}
};
class Child {
std::weak_ptr<Parent> parent_; // Weak reference to avoid cycle
public:
void set_parent(std::shared_ptr<Parent> parent) {
parent_ = parent;
}
void do_something_with_parent() {
if (auto parent = parent_.lock()) { // Convert weak_ptr to shared_ptr
// Use parent safely
}
}
};

unique_ptr Details

unique_ptr has exclusive ownership and can’t be copied:

std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// std::unique_ptr<int> ptr2 = ptr1; // Error! Can't copy
std::unique_ptr<int> ptr2 = std::move(ptr1); // OK - move ownership
// ptr1 is now null, ptr2 owns the memory
assert(ptr1 == nullptr);
assert(*ptr2 == 42);
// Custom deleters are possible
auto file_deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_deleter)> file(fopen("data.txt", "r"), file_deleter);

shared_ptr Details

shared_ptr uses reference counting for shared ownership:

std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::cout << ptr1.use_count() << std::endl; // 1
{
std::shared_ptr<int> ptr2 = ptr1;
std::cout << ptr1.use_count() << std::endl; // 2
}
std::cout << ptr1.use_count() << std::endl; // 1 again
// When use_count reaches 0, the object is automatically deleted

Real World Example

Here’s how I use smart pointers in a simple game engine:

class GameObject {
std::string name_;
std::vector<std::unique_ptr<Component>> components_;
std::weak_ptr<Scene> scene_; // Non-owning reference to parent scene
public:
GameObject(const std::string& name) : name_(name) {}
template<typename T, typename... Args>
T* add_component(Args&&... args) {
auto component = std::make_unique<T>(std::forward<Args>(args)...);
T* raw_ptr = component.get();
components_.push_back(std::move(component));
return raw_ptr;
}
template<typename T>
T* get_component() {
for (auto& comp : components_) {
if (auto typed_comp = dynamic_cast<T*>(comp.get())) {
return typed_comp;
}
}
return nullptr;
}
};
class Scene {
std::vector<std::shared_ptr<GameObject>> objects_;
public:
std::shared_ptr<GameObject> create_object(const std::string& name) {
auto obj = std::make_shared<GameObject>(name);
obj->set_scene(shared_from_this()); // Set weak reference back to scene
objects_.push_back(obj);
return obj;
}
void update() {
// Update all objects - shared_ptr ensures they stay alive during update
for (auto& obj : objects_) {
obj->update();
}
}
};

Common Gotchas

  1. Don’t mix raw and smart pointers:
// Bad - mixing raw and smart pointers
MyClass* raw = new MyClass();
std::unique_ptr<MyClass> smart(raw);
delete raw; // Double deletion when smart destructs
  1. Use make_unique and make_shared:
// Less safe - two allocations for shared_ptr
std::shared_ptr<MyClass> ptr(new MyClass());
// Better - single allocation and exception safe
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
  1. Watch out for circular references with shared_ptr:
// Creates a cycle - objects never get deleted
class Node {
std::shared_ptr<Node> next_;
std::shared_ptr<Node> prev_; // Should be weak_ptr to break cycle
};

The Pattern I Follow

  • Use unique_ptr by default for owned resources
  • Use shared_ptr when multiple owners need the resource
  • Use weak_ptr to observe without owning or to break cycles
  • Always use make_unique and make_shared
  • Avoid raw new/delete completely
// My typical patterns:
class MyClass {
std::unique_ptr<ResourceImpl> impl_; // Exclusive ownership
std::shared_ptr<Cache> cache_; // Shared resource
std::weak_ptr<Parent> parent_; // Non-owning observer
public:
MyClass() : impl_(std::make_unique<ResourceImpl>()) {}
};

Smart pointers eliminate most memory management bugs and make ownership semantics explicit. Once you start using them, going back to manual memory management feels like a step down.