C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete
이 글의 핵심
shared_ptr vs unique_ptr: prefer unique_ptr by default; use shared_ptr for shared ownership. Reference counting cost, weak_ptr for cycles, and performance-minded patterns.
Why Smart Pointers?
Raw pointers in C++ do not communicate ownership. Who frees the memory? Who is allowed to use the pointer after a function call? These questions cause memory leaks, use-after-free, and double-free bugs.
Smart pointers from <memory> make ownership explicit and automatic:
// Raw pointer — ownership unclear
int* raw = new int(42);
delete raw; // remember to delete, no double-delete, no forget
// unique_ptr — single owner, deleted automatically
std::unique_ptr<int> u = std::make_unique<int>(42);
// deleted when u goes out of scope — no manual delete
// shared_ptr — reference counted, deleted when last owner is gone
std::shared_ptr<int> s = std::make_shared<int>(42);
auto s2 = s; // refcount = 2
// deleted when both s and s2 go out of scope
The difference between them is ownership semantics, not just syntax.
Ownership Models
unique_ptr: Exclusive Ownership
unique_ptr expresses that exactly one object owns the resource. You cannot copy it — only move it. When the unique_ptr is destroyed, the object is deleted.
#include <memory>
#include <iostream>
struct Widget {
int id;
Widget(int id) : id(id) { std::cout << "Widget " << id << " created\n"; }
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};
int main() {
auto w = std::make_unique<Widget>(1); // created
// auto w2 = w; // compile error — cannot copy
auto w2 = std::move(w); // ownership transferred
// w is now null — w2 owns the Widget
if (!w) std::cout << "w is empty\n"; // w is empty
std::cout << "w2 owns Widget " << w2->id << '\n';
}
// Widget 1 destroyed — when w2 goes out of scope
This is the most common smart pointer — use it whenever a resource has a single clear owner.
shared_ptr: Shared Ownership
shared_ptr allows multiple owners. A reference count in the control block tracks how many shared_ptr objects point to the resource. The resource is deleted when the last shared_ptr to it is destroyed.
#include <memory>
#include <iostream>
int main() {
auto shared = std::make_shared<Widget>(2); // refcount = 1
std::cout << "count: " << shared.use_count() << '\n'; // 1
{
auto copy1 = shared; // refcount = 2
auto copy2 = shared; // refcount = 3
std::cout << "count: " << shared.use_count() << '\n'; // 3
} // copy1 and copy2 destroyed — refcount = 1
std::cout << "count: " << shared.use_count() << '\n'; // 1
}
// Widget 2 destroyed — last owner (shared) goes out of scope
weak_ptr: Non-Owning Observer
weak_ptr observes a shared_ptr-managed object without participating in ownership. It does not keep the object alive.
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<Widget> owner = std::make_shared<Widget>(3);
std::weak_ptr<Widget> observer = owner; // does not increment refcount
std::cout << "owner count: " << owner.use_count() << '\n'; // 1
// To use a weak_ptr, lock() it — returns shared_ptr or empty
if (auto locked = observer.lock()) {
std::cout << "Object still alive: " << locked->id << '\n';
}
owner.reset(); // Widget destroyed
if (observer.expired()) {
std::cout << "Object has been destroyed\n";
}
}
Overhead Comparison
unique_ptr has zero overhead compared to a raw pointer — it is the same size and often compiles to identical machine code:
unique_ptr<T>: [T*] — same size as a raw pointer
— no heap allocation for ownership tracking
— custom deleter encoded in the type
shared_ptr<T>: [T*, control*] — two pointers
— control block: refcount (atomic) + weakcount + deleter
— control block is a separate heap allocation unless make_shared
make_shared is more efficient than shared_ptr(new T(...)) because it allocates the object and control block in a single allocation:
// Two allocations: one for Widget, one for control block
std::shared_ptr<Widget> p1(new Widget(1));
// One allocation: Widget and control block together
std::shared_ptr<Widget> p2 = std::make_shared<Widget>(2);
The refcount updates in shared_ptr use atomic operations, which have measurable cost on multicore systems. In a tight loop copying and destroying shared_ptr, this can be slower than a similar unique_ptr workload. Profile before assuming it matters in your case.
Usage Patterns
Passing unique_ptr to Functions
// Sink: function takes ownership — pass by value, caller must move
void consume(std::unique_ptr<Widget> w) {
std::cout << "consuming " << w->id << '\n';
// w deleted when function returns
}
// Borrow: function uses but doesn't own — pass raw pointer or const reference
void inspect(const Widget& w) {
std::cout << "inspecting " << w.id << '\n';
}
void inspectPtr(const Widget* w) {
if (w) std::cout << "inspecting " << w->id << '\n';
}
int main() {
auto w = std::make_unique<Widget>(10);
inspect(*w); // borrow by reference — w still owns
inspectPtr(w.get()); // borrow by raw pointer — w still owns
consume(std::move(w)); // transfer ownership — w is now null
// w is null here
}
Factory Functions — Return unique_ptr
Factories that create objects should return unique_ptr. The caller can either keep it as unique_ptr, or upgrade to shared_ptr if needed:
std::unique_ptr<Widget> makeWidget(int id) {
return std::make_unique<Widget>(id); // clear: factory gives you ownership
}
int main() {
// Keep as unique_ptr
auto w = makeWidget(1);
// Upgrade to shared_ptr if shared ownership is needed later
std::shared_ptr<Widget> shared = makeWidget(2);
}
Custom Deleters
Both smart pointer types support custom deletion logic:
#include <cstdio>
#include <memory>
// unique_ptr with custom deleter — deleter is part of the type
auto fileCloser = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileCloser)> file(fopen("data.txt", "r"), fileCloser);
// shared_ptr with custom deleter — deleter is type-erased, not in the type
std::shared_ptr<FILE> sharedFile(fopen("data.txt", "r"), [](FILE* f) {
if (f) fclose(f);
});
The difference: unique_ptr’s deleter is part of the template type (visible to callers), while shared_ptr’s deleter is stored in the control block and erased from the type. shared_ptr is more flexible here — you can store different deleters in the same shared_ptr<FILE> container.
Fixing Circular Reference with weak_ptr
The most common shared_ptr bug is a reference cycle. Two objects pointing to each other with shared_ptr keep each other alive forever — memory leak:
#include <memory>
#include <iostream>
struct Node {
int value;
std::shared_ptr<Node> next; // shared_ptr to next
std::shared_ptr<Node> prev; // shared_ptr to previous — PROBLEM
Node(int v) : value(v) {}
~Node() { std::cout << "Node " << value << " destroyed\n"; }
};
int main() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // cycle: n1 → n2 → n1
// End of main: n1 refcount = 1 (n2->prev), n2 refcount = 1 (n1->next)
// Neither reaches zero — LEAK. Neither destructor is called.
}
Fix: use weak_ptr on the back edge (the one that doesn’t need to keep the object alive):
struct Node {
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak_ptr — no ownership
Node(int v) : value(v) {}
~Node() { std::cout << "Node " << value << " destroyed\n"; }
};
int main() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr — does not increment n1's refcount
// End of main: n1 refcount = 1 (stack), n2 refcount = 1 (n1->next)
// n1 destroyed: refcount drops to 0 → Node 1 destroyed
// Then n1->next released: n2 refcount drops to 0 → Node 2 destroyed
}
// Output:
// Node 1 destroyed
// Node 2 destroyed
Thread Safety
shared_ptr guarantees that reference count modifications (copying and destroying shared_ptr instances) are thread-safe. But the pointed-to object is not:
#include <memory>
#include <thread>
#include <mutex>
auto shared = std::make_shared<int>(0);
std::mutex mtx;
void increment() {
// shared_ptr copy/destroy is thread-safe
auto local = shared; // safe — atomic refcount increment
// BUT: modifying the pointed-to int is NOT thread-safe without synchronization
std::lock_guard<std::mutex> lock(mtx);
(*local)++;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << *shared << '\n'; // 2 — correct with mutex
}
Selection Guide
| Situation | Use |
|---|---|
| Single clear owner, resource returned from factory | unique_ptr |
| Multiple independent owners that outlive each other | shared_ptr |
| Observer: needs access but not ownership | weak_ptr or raw reference |
| Resource with custom cleanup (file, socket, lock) | unique_ptr with deleter |
| Doubly-linked structure, parent-child with back-pointer | shared_ptr forward, weak_ptr back |
| Polymorphic base class pointer | unique_ptr<Base> or shared_ptr<Base> |
| Container of heterogeneous owned objects | vector<unique_ptr<Base>> |
Default rule: reach for unique_ptr first. Upgrade to shared_ptr only when the use case genuinely requires shared ownership — when it’s difficult to define a single owner with a clear lifetime.
enable_shared_from_this
When an object managed by shared_ptr needs to create a new shared_ptr to itself (common in async callbacks), use enable_shared_from_this:
#include <memory>
#include <iostream>
class Session : public std::enable_shared_from_this<Session> {
public:
void start() {
// shared_from_this() returns a shared_ptr to this — safe
auto self = shared_from_this();
// pass self to async callback to keep Session alive
doAsync([self]() {
std::cout << "async work on Session\n";
});
}
template<typename F>
void doAsync(F f) { f(); }
};
int main() {
auto session = std::make_shared<Session>();
session->start();
}
Never call shared_from_this() in a constructor — the shared_ptr managing the object doesn’t exist yet.
Key Takeaways
- Default to
unique_ptr— zero overhead, clear ownership, move-only semantics prevent accidental sharing - Use
shared_ptronly when ownership is genuinely shared with independently-scoped owners make_unique/make_sharedare preferred: exception-safe and (formake_shared) a single allocationweak_ptrbreaks reference cycles and allows non-owning observation — always lock before use- Thread safety:
shared_ptrcopy/destroy is thread-safe; the pointed-to object is not - Custom deleters:
unique_ptrencodes in the type;shared_ptrtype-erases it in the control block - Factory functions should return
unique_ptr— callers can upgrade toshared_ptrif needed
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. shared_ptr vs unique_ptr: prefer unique_ptr by default; use shared_ptr for shared ownership.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Finding C++ Memory Leaks: Valgrind, AddressSanitizer, and
- C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe
- C++ Memory Leaks
- C++ Shallow vs Deep Copy & Move Semantics Complete Guide
Keywords Covered in This Article (Related Search Terms)
This article covers C++, smart pointers, shared_ptr, unique_ptr, weak_ptr, memory management, RAII.