Appearance
Smart Pointers
Video: Resource Acquisition is Initialization (RAII) Explained in C++
Smart pointers are modern C++ RAII implementations that automatically manage memory allocation and deallocation. They solve the problems of manual memory management by providing automatic cleanup, exception safety, and clear ownership semantics.
Why Smart Pointers?
Traditional raw pointers have several problems:
cpp
// Problems with raw pointers
void problematic_function() {
int* ptr = new int(42);
if (some_condition) {
return; // MEMORY LEAK! forgot to delete
}
risky_operation(); // If this throws, memory leaks
delete ptr; // May never be reached
}Smart pointers solve these issues:
cpp
// Safe with smart pointers
void safe_function() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
if (some_condition) {
return; // Memory automatically freed
}
risky_operation(); // Exception safe - memory still freed
// Memory automatically freed here too
}Additionally, raw pointers do not have ownership semantics. They are just pointers to memory locations. When you call a function that returns a raw pointer, you have no way of knowing who is responsible for managing the lifetime of the resource and whether it is safe to use the pointer. Smart pointers help alleviate this problem by providing ownership semantics and automatically freeing the resource when the smart pointer goes out of scope.
Types of Smart Pointers
1. std::unique_ptr - Exclusive Ownership
std::unique_ptr represents exclusive ownership of a resource. Only one unique_ptr can own a resource at a time. It is a zero-cost abstraction over raw pointers i.e. all the operations on unique_ptr are optimized out by the compiler.
cpp
#include <memory>
// Create unique_ptr
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2(new int(100)); // Also valid, but prefer make_unique
// Access the value
std::cout << *ptr1 << std::endl; // 42
std::cout << ptr1.get() << std::endl; // Raw pointer address
// Check if it owns something
if (ptr1) {
std::cout << "ptr1 owns a resource" << std::endl;
}unique_ptr is a move-only type, so it cannot be copied or assigned. It can only be moved. Semantically, this is equivalent to moving the ownership of the resource to the new unique_ptr. You can also add custom deleters to unique_ptr to control how the resource is freed. This allows you to write your own RAII classes that manage resources like files, sockets, or other system resources using just unique_ptr.
It literally costs you nothing to use unique_ptr and adds ownership semantics to your code! Ensure you use unique_ptr by default.
2. std::shared_ptr - Shared Ownership
std::shared_ptr allows multiple pointers to share ownership of the same resource. The resource is freed when the last shared_ptr is destroyed.
cpp
// Create shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // Both share ownership
std::cout << "Reference count: " << ptr1.use_count() << std::endl; // 2
std::cout << "Value: " << *ptr1 << std::endl; // 42It does garbage collection by reference counting. The reference count is incremented when a new shared_ptr is created and decremented when a shared_ptr is destroyed. When the reference count reaches 0, the resource is freed. As expected, it is not a zero-cost abstraction over raw pointers, since it has to maintain the reference count! In-fact, the reference count is stored in the heap and is thread-safe, which further adds to the overhead of using shared_ptr.
You can also add custom deleters to shared_ptr to control how the resource is freed.
3. std::weak_ptr - Non-Owning Observer
std::weak_ptr provides a non-owning "weak" reference to an object managed by shared_ptr. It doesn't affect the reference count and can detect if the object has been deleted.
cpp
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared; // Weak reference
std::cout << "Shared count: " << shared.use_count() << std::endl; // 1
std::cout << "Weak count: " << weak.use_count() << std::endl; // 1
// Check if object still exists
if (!weak.expired()) {
// Convert to shared_ptr to access
if (auto locked = weak.lock()) {
std::cout << "Value: " << *locked << std::endl; // 42
}
}