Usage Scenarios and Differences of C++ Smart Pointers
C++11 introduced smart pointers for automatic management of dynamically allocated memory, avoiding memory leaks and dangling pointer issues. Smart pointers are a typical application of the RAII (Resource Acquisition Is Initialization) pattern.
Three Main Smart Pointers
1. std::unique_ptr
- Exclusive ownership smart pointer
- Only one unique_ptr can point to an object at any given time
- Cannot be copied, only moved
- Lightweight with almost no overhead
2. std::shared_ptr
- Shared ownership smart pointer
- Multiple shared_ptrs can point to the same object
- Uses reference counting to manage object lifetime
- Can be copied and moved
3. std::weak_ptr
- Smart pointer that doesn't control object lifetime
- Must be used with shared_ptr
- Used to solve circular reference problems with shared_ptr
- Does not increase reference count
Usage Scenarios
std::unique_ptr applicable scenarios:
- Object ownership is clear and doesn't need to be shared
- Returning dynamically allocated objects as function return values
- Storing dynamically allocated objects in containers
- Pimpl (Pointer to Implementation) pattern
std::shared_ptr applicable scenarios:
- Multiple objects need to share the same resource
- Object lifetime is uncertain and needs delayed release
- Passing objects in asynchronous programming
- Sharing resources in cache systems
std::weak_ptr applicable scenarios:
- Avoiding circular references in observer patterns
- Avoiding strong references causing resource unavailability in cache systems
- Breaking circular references in shared_ptr
Code Examples
std::unique_ptr example:
cpp#include <memory> #include <iostream> class Widget { public: Widget() { std::cout << "Widget constructed\n"; } ~Widget() { std::cout << "Widget destroyed\n"; } void doSomething() { std::cout << "Widget doing something\n"; } }; // Create unique_ptr std::unique_ptr<Widget> ptr1 = std::make_unique<Widget>(); // Move semantics std::unique_ptr<Widget> ptr2 = std::move(ptr1); // ptr1 is now nullptr // Custom deleter auto deleter = [](Widget* w) { std::cout << "Custom deleter\n"; delete w; }; std::unique_ptr<Widget, decltype(deleter)> ptr3(new Widget(), deleter); // As function parameter void processWidget(std::unique_ptr<Widget> ptr) { ptr->doSomething(); } processWidget(std::move(ptr2));
std::shared_ptr example:
cpp#include <memory> #include <iostream> class Resource { public: Resource() { std::cout << "Resource created\n"; } ~Resource() { std::cout << "Resource destroyed\n"; } }; // Create shared_ptr std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>(); std::cout << "Use count: " << ptr1.use_count() << "\n"; // 1 // Copy std::shared_ptr<Resource> ptr2 = ptr1; std::cout << "Use count: " << ptr1.use_count() << "\n"; // 2 // Move std::shared_ptr<Resource> ptr3 = std::move(ptr1); std::cout << "Use count: " << ptr2.use_count() << "\n"; // 2 std::cout << "ptr1 is " << (ptr1 ? "not null" : "null") << "\n"; // null // Custom deleter auto deleter = [](Resource* r) { std::cout << "Custom deleter for Resource\n"; delete r; }; std::shared_ptr<Resource> ptr4(new Resource(), deleter);
std::weak_ptr example:
cpp#include <memory> #include <iostream> class Node { public: std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // Use weak_ptr to avoid circular reference Node() { std::cout << "Node created\n"; } ~Node() { std::cout << "Node destroyed\n"; } }; void createCycle() { auto node1 = std::make_shared<Node>(); auto node2 = std::make_shared<Node>(); node1->next = node2; node2->prev = node1; // weak_ptr doesn't increase reference count // node1 and node2 can be properly released } // Use weak_ptr to check if object exists std::weak_ptr<int> weakPtr; { auto sharedPtr = std::make_shared<int>(42); weakPtr = sharedPtr; if (auto locked = weakPtr.lock()) { std::cout << "Value: " << *locked << "\n"; // 42 } else { std::cout << "Object has been destroyed\n"; } } // sharedPtr goes out of scope, object is destroyed if (auto locked = weakPtr.lock()) { std::cout << "Value: " << *locked << "\n"; } else { std::cout << "Object has been destroyed\n"; // Executes here }
Performance Comparison
Memory overhead:
- unique_ptr: Almost no overhead, only contains a raw pointer
- shared_ptr: Contains two pointers (control block + object pointer), about 16 bytes
- weak_ptr: Similar to shared_ptr, contains control block pointer
Operation overhead:
- unique_ptr: All operations are O(1), no additional overhead
- shared_ptr: Copy and destruction require atomic operations to modify reference count
- weak_ptr: lock() operation requires atomic operations
Circular Reference Problem
Problem example:
cppclass A { public: std::shared_ptr<B> b_ptr; }; class B { public: std::shared_ptr<A> a_ptr; // Causes circular reference }; auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b_ptr = b; b->a_ptr = a; // Reference counts of a and b never become 0, memory leak
Solution:
cppclass B { public: std::weak_ptr<A> a_ptr; // Use weak_ptr to break the cycle };
Best Practices
1. Prefer std::make_unique and std::make_shared
cpp// Recommended auto ptr = std::make_unique<Widget>(); auto ptr = std::make_shared<Resource>(); // Not recommended auto ptr = std::unique_ptr<Widget>(new Widget()); auto ptr = std::shared_ptr<Resource>(new Resource());
2. Use std::move to transfer unique_ptr ownership
cppstd::unique_ptr<Widget> createWidget() { return std::make_unique<Widget>(); } auto ptr = createWidget(); // Move construction
3. Avoid creating shared_ptr from raw pointers
cppint* raw = new int(42); std::shared_ptr<int> ptr1(raw); std::shared_ptr<int> ptr2(raw); // Wrong! Will cause double deletion
4. Use std::enable_shared_from_this
cppclass MyClass : public std::enable_shared_from_this<MyClass> { public: std::shared_ptr<MyClass> getShared() { return shared_from_this(); } }; auto obj = std::make_shared<MyClass>(); auto shared = obj->getShared();
Notes
- Don't mix raw pointers and smart pointers to manage the same object
- Be aware of thread safety when using shared_ptr in multi-threaded environments
- weak_ptr::lock() can return an empty shared_ptr
- Avoid frequently creating and destroying shared_ptr in loops
- Consider using std::observer_ptr (C++26) as an alternative for non-owning pointers