Smart Pointers
/ 4 min read
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 managementint* raw_ptr = new int(42);// ... use the pointerdelete raw_ptr; // Easy to forget!
// New way - automatic cleanupstd::unique_ptr<int> smart_ptr = std::make_unique<int>(42);// ... use the pointer// Automatically deleted when smart_ptr goes out of scopeNo 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 ownershipstd::unique_ptr<MyClass> unique = std::make_unique<MyClass>();
// shared_ptr - shared ownership with reference countingstd::shared_ptr<MyClass> shared = std::make_shared<MyClass>();
// weak_ptr - non-owning observer to break circular referencesstd::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:
- 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};- 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();}- 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 deviceauto audio_device = std::make_shared<AudioDevice>();AudioSystem music_system(audio_device);AudioSystem sfx_system(audio_device); // Shares the same device- 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 copystd::unique_ptr<int> ptr2 = std::move(ptr1); // OK - move ownership
// ptr1 is now null, ptr2 owns the memoryassert(ptr1 == nullptr);assert(*ptr2 == 42);
// Custom deleters are possibleauto 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 deletedReal 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
- Don’t mix raw and smart pointers:
// Bad - mixing raw and smart pointersMyClass* raw = new MyClass();std::unique_ptr<MyClass> smart(raw);delete raw; // Double deletion when smart destructs- Use make_unique and make_shared:
// Less safe - two allocations for shared_ptrstd::shared_ptr<MyClass> ptr(new MyClass());
// Better - single allocation and exception safestd::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();- Watch out for circular references with shared_ptr:
// Creates a cycle - objects never get deletedclass Node { std::shared_ptr<Node> next_; std::shared_ptr<Node> prev_; // Should be weak_ptr to break cycle};The Pattern I Follow
- Use
unique_ptrby default for owned resources - Use
shared_ptrwhen multiple owners need the resource - Use
weak_ptrto observe without owning or to break cycles - Always use
make_uniqueandmake_shared - Avoid raw
new/deletecompletely
// 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.