Smart Pointers in C++: unique_ptr, shared_ptr, and weak_ptr
Manual memory management in C++ has long been a source of bugs, memory leaks, and undefined behavior. Smart pointers were introduced in C++11 to solve these problems by automating resource management. In this post, we'll explore the three main smart pointers: unique_ptr, shared_ptr, and weak_ptr, with practical examples of their use cases.
Why Smart Pointers Matter
Before smart pointers, C++ developers had to manually manage memory:
// Manual memory management - error-prone
void processData() {
MyClass* obj = new MyClass();
// ... do something with obj ...
// What if we forget this?
delete obj; // Memory leak if exception thrown or early return
}
Smart pointers ensure automatic cleanup when objects go out of scope, eliminating memory leaks and making code exception-safe.
unique_ptr: Exclusive Ownership
std::unique_ptr represents exclusive ownership of a dynamically allocated object. Only one unique_ptr can own an object at a time, and ownership can be transferred but not shared.
Basic Usage
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl; // Outputs: 42
// Automatic cleanup when ptr goes out of scope
Ownership Transfer
std::unique_ptr<int> ptr1 = std::make_unique<int>(100);
// Transfer ownership
std::unique_ptr<int> ptr2 = std::move(ptr1);
// ptr1 is now nullptr
assert(ptr1 == nullptr);
assert(*ptr2 == 100);
Use Cases for unique_ptr
1. Factory Functions
std::unique_ptr<Connection> createConnection(const std::string& url) {
// Complex initialization logic
auto conn = std::make_unique<Connection>();
conn->connect(url);
return conn; // Ownership transferred to caller
}
void clientCode() {
auto connection = createConnection("tcp://example.com:8080");
// Use connection...
// Automatic cleanup
}
2. Resource Management (RAII)
class FileHandler {
private:
std::unique_ptr<FILE, decltype(&fclose)> file_;
public:
FileHandler(const char* filename)
: file_(fopen(filename, "r"), fclose) {
if (!file_) {
throw std::runtime_error("Failed to open file");
}
}
void process() {
// File automatically closed when FileHandler is destroyed
char buffer[1024];
while (fgets(buffer, sizeof(buffer), file_.get())) {
// Process buffer
}
}
};
3. Polymorphism and Inheritance
class Animal { public: virtual void speak() = 0; virtual ~Animal() = default; };
class Dog : public Animal { public: void speak() override { std::cout << "Woof!\n"; } };
class Cat : public Animal { public: void speak() override { std::cout << "Meow!\n"; } };
std::unique_ptr<Animal> createPet(const std::string& type) {
if (type == "dog") {
return std::make_unique<Dog>();
} else if (type == "cat") {
return std::make_unique<Cat>();
}
return nullptr;
}
void petShow() {
auto pet = createPet("dog");
pet->speak(); // Outputs: Woof!
}
shared_ptr: Shared Ownership
std::shared_ptr enables shared ownership of objects. Multiple shared_ptr instances can point to the same object, and the object is destroyed only when the last shared_ptr is destroyed.
Reference Counting
shared_ptr uses reference counting to track how many pointers own the object:
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 << ptr2.use_count() << std::endl; // 2
ptr2.reset();
std::cout << ptr1.use_count() << std::endl; // 1
// Object destroyed when ptr1 goes out of scope
Use Cases for shared_ptr
1. Multiple Owners
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
class Subject {
private:
std::vector<std::shared_ptr<Observer>> observers_;
public:
void addObserver(std::shared_ptr<Observer> observer) {
observers_.push_back(observer);
}
void notify() {
for (auto& obs : observers_) {
obs->update();
}
}
};
class ConcreteObserver : public Observer {
public:
void update() override {
std::cout << "Observer notified\n";
}
};
void observerPattern() {
auto subject = std::make_shared<Subject>();
auto observer1 = std::make_shared<ConcreteObserver>();
auto observer2 = std::make_shared<ConcreteObserver>();
subject->addObserver(observer1);
subject->addObserver(observer2);
subject->notify(); // Both observers get notified
// All objects cleaned up automatically
}
2. Caches and Flyweights
class Texture {
public:
Texture(const std::string& path) { /* load texture */ }
// ...
};
class TextureManager {
private:
std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;
public:
std::shared_ptr<Texture> getTexture(const std::string& path) {
// Check if texture is already loaded
if (auto cached = cache_[path].lock()) {
return cached;
}
// Load new texture
auto texture = std::make_shared<Texture>(path);
cache_[path] = texture;
return texture;
}
};
3. Graph Structures
struct Node {
int data;
std::vector<std::shared_ptr<Node>> children;
Node(int val) : data(val) {}
};
void buildTree() {
auto root = std::make_shared<Node>(1);
auto child1 = std::make_shared<Node>(2);
auto child2 = std::make_shared<Node>(3);
root->children.push_back(child1);
root->children.push_back(child2);
// All nodes automatically cleaned up
}
weak_ptr: Breaking Reference Cycles
std::weak_ptr is used to break circular references between shared_ptr objects. It doesn't contribute to the reference count and can only access the object if it still exists.
Basic Usage
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
std::cout << weak.use_count() << std::endl; // 1 (only the shared_ptr counts)
// Access the object
if (auto locked = weak.lock()) {
std::cout << *locked << std::endl; // 42
} else {
std::cout << "Object destroyed\n";
}
Use Cases for weak_ptr
1. Breaking Circular References
class Parent;
class Child;
class Parent {
public:
std::shared_ptr<Child> child;
~Parent() { std::cout << "Parent destroyed\n"; }
};
class Child {
public:
std::weak_ptr<Parent> parent; // weak_ptr to break cycle
~Child() { std::cout << "Child destroyed\n"; }
};
void circularReference() {
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent; // weak_ptr doesn't create cycle
// Objects are properly destroyed when they go out of scope
}
2. Observer Pattern (Safe Version)
class Subject;
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers_; // weak_ptrs
public:
void addObserver(std::shared_ptr<Observer> observer) {
observers_.push_back(observer); // weak_ptr from shared_ptr
}
void notify() {
// Remove expired observers
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[](const std::weak_ptr<Observer>& wp) { return wp.expired(); }),
observers_.end()
);
for (auto& wp : observers_) {
if (auto obs = wp.lock()) {
obs->update();
}
}
}
};
3. Cache with Automatic Cleanup
class Cache {
private:
std::unordered_map<std::string, std::weak_ptr<Data>> cache_;
public:
void store(const std::string& key, std::shared_ptr<Data> data) {
cache_[key] = data;
}
std::shared_ptr<Data> get(const std::string& key) {
auto it = cache_.find(key);
if (it != cache_.end()) {
return it->second.lock(); // May return nullptr if expired
}
return nullptr;
}
void cleanup() {
for (auto it = cache_.begin(); it != cache_.end(); ) {
if (it->second.expired()) {
it = cache_.erase(it);
} else {
++it;
}
}
}
};
Performance Considerations
| Smart Pointer | Overhead | Use When |
|---|---|---|
unique_ptr |
Zero runtime cost | Single ownership, performance-critical code |
shared_ptr |
Reference counting (atomic) | Shared ownership, complex object graphs |
weak_ptr |
Minimal (tracks shared_ptr) | Breaking cycles, optional references |
Best Practices
- Prefer
std::make_uniqueandstd::make_sharedfor exception safety - Use
unique_ptrby default for single ownership - Use
shared_ptronly when sharing is necessary - Use
weak_ptrto break cycles and for optional references - Avoid raw pointers in modern C++ code
- Consider object ownership when choosing pointer types
Common Pitfalls
Circular References
// BAD: Circular reference causes memory leak
class A { std::shared_ptr<B> b; };
class B { std::shared_ptr<A> a; };
// GOOD: Use weak_ptr to break the cycle
class A { std::shared_ptr<B> b; };
class B { std::weak_ptr<A> a; };
Premature Optimization
// Don't do this for performance reasons
std::unique_ptr<int> ptr(new int(42)); // Less efficient
// Do this instead
auto ptr = std::make_unique<int>(42); // Better exception safety
Conclusion
Smart pointers are one of C++'s most important features for writing safe, maintainable code. Understanding when to use each type is crucial:
unique_ptr: For exclusive ownership and RAIIshared_ptr: For shared ownership and complex relationshipsweak_ptr: For breaking cycles and optional references
By using smart pointers appropriately, you can eliminate memory leaks, make your code exception-safe, and express ownership semantics clearly. Start with unique_ptr and only reach for shared_ptr when you truly need shared ownership.