본문으로 건너뛰기
Previous
Next
C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete

C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete

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

SituationUse
Single clear owner, resource returned from factoryunique_ptr
Multiple independent owners that outlive each othershared_ptr
Observer: needs access but not ownershipweak_ptr or raw reference
Resource with custom cleanup (file, socket, lock)unique_ptr with deleter
Doubly-linked structure, parent-child with back-pointershared_ptr forward, weak_ptr back
Polymorphic base class pointerunique_ptr<Base> or shared_ptr<Base>
Container of heterogeneous owned objectsvector<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_ptr only when ownership is genuinely shared with independently-scoped owners
  • make_unique / make_shared are preferred: exception-safe and (for make_shared) a single allocation
  • weak_ptr breaks reference cycles and allows non-owning observation — always lock before use
  • Thread safety: shared_ptr copy/destroy is thread-safe; the pointed-to object is not
  • Custom deleters: unique_ptr encodes in the type; shared_ptr type-erases it in the control block
  • Factory functions should return unique_ptr — callers can upgrade to shared_ptr if needed

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. shared_ptr vs unique_ptr: prefer unique_ptr by default; use shared_ptr for shared ownership. Reference counting cost, we… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [Finding C++ Memory Leaks: Valgrind, AddressSanitizer, and](/en/blog/cpp-error-07-memory-leak-detection/
  • [C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe](/en/blog/cpp-smart-pointers/
  • [C++ Memory Leaks](/en/blog/cpp-series-06-2-memory-leak/
  • [C++ Shallow vs Deep Copy & Move Semantics Complete Guide](/en/blog/cpp-series-33-2-copy-move/

이 글에서 다루는 키워드 (관련 검색어)

C++, smart pointers, shared_ptr, unique_ptr, weak_ptr, memory management, RAII 등으로 검색하시면 이 글이 도움이 됩니다.