C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing

C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing

이 글의 핵심

this를 shared_ptr로 감싸다 이중 해제로 크래시한 적 있나요? enable_shared_from_this, aliasing 생성자, shared_ptr 스레드 안전성, 멤버 포인터 수명 관리까지. 실전 문제 시나리오와 프로덕션 패턴.

들어가며: this를 shared_ptr로 감싸다 크래시한 적 있나요?

”비동기 콜백에서 this를 쓰다 use-after-free가 났어요”

shared_ptr 기초를 익힌 뒤, 실전에서는 this를 shared_ptr로 감싸서 비동기 콜백에 넘기거나, 부모 객체의 멤버 포인터를 shared_ptr로 반환하는 상황이 자주 발생합니다. 이때 잘못된 패턴을 쓰면 이중 해제(double-free)use-after-free로 크래시가 납니다.

비유하면: “내 집 열쇠를 복제해서 친구에게 주려는데, 원본 열쇠와 복제 열쇠가 서로 다른 관리 시스템을 쓰면, 한쪽이 문을 닫았다고 해서 다른 쪽이 또 닫으려다 충돌하는 것”과 같습니다. enable_shared_from_this는 “이미 shared_ptr로 관리 중인 객체”에서 안전하게 shared_ptr을 얻는 방법을 제공합니다.

이 글을 읽으면:

  • enable_shared_from_this로 this를 안전하게 shared_ptr로 변환할 수 있습니다.
  • aliasing 생성자로 부모 수명에 묶인 멤버 포인터를 반환할 수 있습니다.
  • 커스텀 삭제자로 FILE*, 배열 등 delete가 아닌 리소스를 관리할 수 있습니다.
  • atomic<shared_ptr>과 weak_ptr 옵저버 패턴으로 멀티스레드·이벤트 시스템을 구현할 수 있습니다.
  • shared_ptr의 스레드 안전성 범위를 정확히 이해할 수 있습니다.
  • 자주 하는 실수와 프로덕션 패턴을 익힐 수 있습니다.

목차

  1. 문제 시나리오
  2. enable_shared_from_this 완전 가이드
  3. aliasing 생성자
  4. 커스텀 삭제자 (Custom Deleter)
  5. shared_ptr 스레드 안전성
  6. 완전한 예제 코드
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례
  9. 프로덕션 패턴
  10. 성능과 체크리스트

1. 문제 시나리오

시나리오 1: “비동기 콜백에서 this를 캡처했다가 use-after-free예요”

"네트워크 요청 완료 시 this->onComplete()를 호출하는데,
요청이 끝나기 전에 객체가 삭제되면 크래시해요."

상황: AsyncService가 비동기 API를 호출하고, 완료 시 this를 람다에 캡처합니다. 호출자가 AsyncService를 스택에 두고 함수를 반환하면 객체가 파괴되는데, 비동기 콜백은 아직 대기 중입니다. 콜백이 실행될 때 이미 해제된 this를 사용해 use-after-free가 발생합니다.

해결 포인트: enable_shared_from_this를 상속하고, 콜백에 shared_from_this()를 넘깁니다. shared_ptr이 하나라도 남아 있는 한 객체가 해제되지 않습니다.

시나리오 2: “부모 객체의 멤버를 shared_ptr로 반환하고 싶어요”

"Container가 Bar 멤버를 가지고 있는데,
shared_ptr<Bar>를 반환하면서 Container 수명도 유지되게 하고 싶어요."

상황: shared_ptr<Container>가 있고, Container의 멤버 Bar*shared_ptr<Bar>로 반환해야 합니다. shared_ptr<Bar>(container->member)처럼 만들면 새로운 제어 블록이 생겨, ContainerBar의 수명이 분리됩니다. Container가 먼저 삭제되면 Bar는 이미 해제된 메모리를 가리키게 됩니다.

해결 포인트: aliasing 생성자 shared_ptr<Bar>(container, &container->member)를 사용하면, BarContainer의 수명에 묶입니다. Container가 살아 있는 한 Bar에 안전하게 접근할 수 있습니다.

시나리오 3: “여러 스레드가 shared_ptr을 공유하는데 데이터 레이스가 나요”

"스레드 A와 B가 같은 shared_ptr을 동시에 reset하면 크래시해요."
"참조 카운트는 스레드 안전한데, 왜 문제가 생기나요?"

상황: shared_ptr의 참조 카운트 증감은 원자적이지만, shared_ptr 객체 자체를 여러 스레드가 동시에 수정(복사 대입, reset 등)하면 데이터 레이스가 발생합니다. 같은 shared_ptr 변수를 여러 스레드가 공유하면서 수정하는 것이 문제입니다.

해결 포인트: 각 스레드가 자기만의 복사본을 가지거나, std::atomic<std::shared_ptr<T>>(C++20)를 사용합니다.

시나리오 4: “shared_ptr로 생성되지 않은 객체에서 shared_from_this()를 호출했어요”

"스택에 선언한 객체에서 shared_from_this()를 호출했더니
std::bad_weak_ptr 예외가 났어요."

상황: enable_shared_from_this를 상속했어도, 객체가 shared_ptr로 생성되지 않으면 내부 weak_ptr이 초기화되지 않습니다. shared_from_this()는 이때 std::bad_weak_ptr을 던집니다.

해결 포인트: enable_shared_from_this를 쓰는 클래스는 반드시 std::make_shared<T>() 또는 shared_ptr<T>(new T(...))로 생성합니다. 스택에 두지 않습니다.

시나리오 5: “생성자에서 shared_from_this()를 호출했어요”

"생성자에서 shared_from_this()를 호출했더니
아직 shared_ptr이 없어서 예외가 나요."

상황: 생성자가 실행되는 시점에는 아직 shared_ptr이 객체를 소유하지 않습니다. enable_shared_from_this의 내부 weak_ptr도 아직 설정되지 않아, shared_from_this()를 호출하면 std::bad_weak_ptr이 발생합니다.

해결 포인트: shared_from_this()생성자와 소멸자 밖에서만 호출합니다. 초기화가 필요한 경우 void init() 같은 별도 메서드에서 호출합니다.

시나리오 6: “FILE*나 배열을 shared_ptr로 관리하고 싶어요”

"fopen()으로 연 파일을 shared_ptr이 해제될 때 fclose()로 닫고 싶어요."
"new[]로 할당한 배열을 shared_ptr이 delete[]로 해제하게 하고 싶어요."

상황: shared_ptr은 기본적으로 delete ptr을 호출합니다. FILE*, int[](배열), HANDLE, pthread_mutex_t*delete가 아닌 다른 해제 방식이 필요한 리소스는 기본 shared_ptr로 관리할 수 없습니다. 잘못 사용하면 delete 대신 fclose()가 호출되어야 하는데 delete가 호출되거나, delete 대신 delete[]가 필요한데 delete만 호출되는 미정의 동작이 발생합니다.

해결 포인트: 커스텀 삭제자(Custom Deleter)를 두 번째 인자로 전달합니다. shared_ptr<T>(ptr, deleter) 형태로, deletervoid(ptr) 시그니처의 호출 가능 객체입니다.

시나리오 7: “옵저버가 주제를 참조하다 use-after-free가 나요”

"이벤트 구독자 목록에 shared_ptr로 등록했더니,
구독 해제해도 발행자가 shared_ptr을 들고 있어서 객체가 안 죽어요."
"반대로 weak_ptr을 쓰면, 접근할 때 이미 삭제됐을 수 있어요."

상황: 옵저버 패턴에서 발행자(Subject)가 구독자(Observer) 목록을 vector<shared_ptr<Observer>>로 들면, 구독자가 자기 자신을 해제해도 발행자가 shared_ptr을 유지해 객체가 영원히 살아 있습니다. 반대로 vector<Observer*>로 raw 포인터를 쓰면, 구독자가 먼저 삭제된 뒤 발행자가 콜백을 호출할 때 use-after-free가 발생합니다.

해결 포인트: 발행자는 vector<weak_ptr<Observer>>로 구독자를 저장합니다. 콜백 호출 전에 lock()으로 shared_ptr을 얻고, expired()이면 건너뜁니다. “있으면 쓰고, 없으면 무시”하는 weak_ptr 옵저버 패턴입니다.


2. enable_shared_from_this 완전 가이드

enable_shared_from_this란?

std::enable_shared_from_this<T>를 상속하면, 이미 shared_ptr로 관리 중인 객체에서 shared_from_this()를 통해 동일한 제어 블록을 공유하는 shared_ptr을 얻을 수 있습니다. shared_ptr<T>(this)처럼 새 shared_ptr을 만들면 새 제어 블록이 생겨 이중 해제가 발생합니다.

왜 shared_ptr(this)가 위험한가?

flowchart TB
    subgraph bad["❌ shared_ptr(this) 사용"]
        B1["shared_ptr A"] --> B2["제어 블록 1"]
        B3["shared_ptr B = shared_ptr(this)"] --> B4["제어 블록 2"]
        B2 --> B5["객체"]
        B4 --> B5
        B5 -.->|"각각 독립적으로 delete 시도 → 이중 해제!"| B5
    end

주의사항: shared_ptrthis로 만드는 패턴은 enable_shared_from_this 없이 쓰면 거의 항상 잘못된 설계입니다.

// ❌ 위험한 코드: 이중 해제
class BadService {
public:
    std::shared_ptr<BadService> getShared() {
        return std::shared_ptr<BadService>(this);  // 새 제어 블록!
    }
};

int main() {
    auto p1 = std::make_shared<BadService>();
    auto p2 = p1->getShared();  // p1과 p2는 서로 다른 제어 블록
    // p1 소멸 시 객체 delete
    // p2 소멸 시 같은 객체를 또 delete → 이중 해제 크래시!
    return 0;
}

코드 설명:

  • p1make_shared로 생성되어 제어 블록 1을 가집니다.
  • getShared()shared_ptr(this)를 반환하면 제어 블록 2가 새로 만들어집니다.
  • 두 shared_ptr이 같은 객체를 가리키지만 서로 다른 참조 카운트를 가져, 각각 0이 될 때마다 delete를 시도합니다.
  • 결과: 이중 해제(double-free)로 크래시.

enable_shared_from_this 사용법

#include <memory>
#include <iostream>

class GoodService : public std::enable_shared_from_this<GoodService> {
public:
    std::shared_ptr<GoodService> getShared() {
        return shared_from_this();  // ✅ 동일 제어 블록 공유
    }
    void doWork() { std::cout << "Working\n"; }
};

int main() {
    auto p1 = std::make_shared<GoodService>();
    auto p2 = p1->getShared();  // p1과 p2는 같은 제어 블록
    std::cout << "use_count: " << p1.use_count() << std::endl;  // 2
    return 0;  // p2, p1 순으로 소멸 → 한 번만 delete
}

주의사항: 스택에 둔 객체에서 shared_from_this()를 호출하면 bad_weak_ptr 예외가 나므로, 반드시 make_shared 등으로 힙에 둔 뒤에만 씁니다.

코드 설명:

  • shared_from_this()는 객체가 이미 shared_ptr로 관리될 때만 호출 가능합니다.
  • 반환된 shared_ptr은 원본과 같은 제어 블록을 공유하므로 참조 카운트가 올바르게 동작합니다.
  • 마지막 shared_ptr이 소멸될 때 한 번만 delete됩니다.

비동기 콜백에서의 완전한 예제

#include <memory>
#include <functional>
#include <iostream>
#include <thread>
#include <chrono>

class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
public:
    void startAsync(std::function<void()> onComplete) {
        // ✅ shared_from_this()로 수명 연장
        auto self = shared_from_this();
        std::thread([self, onComplete]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            self->doWork();  // self가 살아 있으므로 안전
            if (onComplete) onComplete();
        }).detach();
    }
private:
    void doWork() { std::cout << "Async work done\n"; }
};

int main() {
    auto worker = std::make_shared<AsyncWorker>();
    worker->startAsync( { std::cout << "Callback\n"; });
    // main이 바로 반환해도 worker가 스레드에 의해 유지됨
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    return 0;
}

실행 결과:

Async work done
Callback

코드 설명:

  • startAsync에서 shared_from_this()self를 얻어 람다에 캡처합니다.
  • 람다가 self를 들고 있으므로, main에서 worker를 놓아도 스레드가 끝날 때까지 객체가 유지됩니다.
  • this를 직접 캡처하면 main 반환 후 use-after-free가 발생합니다.

weak_from_this() (C++17)

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    std::weak_ptr<MyClass> getWeak() {
        return weak_from_this();  // C++17
    }
};

용도: 순환 참조를 피하거나, “있으면 쓰고 없으면 무시”하는 옵저버 패턴에서 사용합니다.

주의사항 요약

조건설명
반드시 shared_ptr로 생성make_shared<T>() 또는 shared_ptr<T>(new T)
생성자/소멸자에서 호출 금지shared_from_this()는 생성·소멸 완료 후에만
public 상속enable_shared_from_this는 반드시 public 상속

3. aliasing 생성자

aliasing 생성자란?

aliasing 생성자shared_ptr<T>(r, ptr) 형태로, r소유권(수명)을 제공하고 ptr실제로 가리킬 대상이 됩니다. 즉, “r이 관리하는 객체의 수명에 묶여 있는 ptr”을 가리키는 shared_ptr을 만듭니다.

구문:

template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
  • r: 소유권을 공유할 shared_ptr (수명 관리)
  • ptr: 역참조 시 사용할 실제 포인터 (r이 관리하는 객체의 일부여야 함)

부모-멤버 수명 바인딩

flowchart LR
    subgraph aliasing["aliasing shared_ptr"]
        A1["shared_ptrContainer"] --> A2["제어 블록"]
        A2 --> A3["Container 객체"]
        A4["shared_ptrBar (aliasing)"] --> A2
        A4 -.->|"가리킴"| A5["Bar (멤버)"]
        A3 --> A5
    end
#include <memory>
#include <iostream>

struct Bar {
    int value = 42;
};

struct Container {
    Bar member;
};

int main() {
    auto container = std::make_shared<Container>();
    container->member.value = 100;

    // ✅ aliasing: container의 수명에 member가 묶임
    std::shared_ptr<Bar> barPtr(container, &container->member);

    std::cout << barPtr->value << std::endl;  // 100
    std::cout << "use_count: " << container.use_count() << std::endl;  // 2

    container.reset();  // container만 해제해도 barPtr이 아직 유효
    std::cout << barPtr->value << std::endl;  // 100 (아직 유효)
    barPtr.reset();    // 이제 Container와 Bar 모두 해제
    return 0;
}

코드 설명:

  • shared_ptr<Bar>(container, &container->member): container의 제어 블록을 공유하면서, 역참조 시 &container->member를 반환합니다.
  • containerbarPtr 모두 같은 참조 카운트를 공유합니다.
  • container를 reset해도 barPtr이 남아 있으면 Container 객체는 해제되지 않습니다.
  • BarContainer의 멤버이므로, Container가 삭제되기 전에는 Bar에 안전하게 접근할 수 있습니다.

멤버 포인터 반환 함수

struct Container {
    Bar member;
    std::shared_ptr<Bar> getMember() {
        return std::shared_ptr<Bar>(shared_from_this(), &member);
    }
};
// 주의: Container가 enable_shared_from_this를 상속해야 함

또는 외부에서 이미 shared_ptr<Container>를 가지고 있다면:

std::shared_ptr<Bar> getBar(std::shared_ptr<Container> c) {
    return std::shared_ptr<Bar>(c, &c->member);
}

aliasing과 enable_shared_from_this

주의: aliasing 생성자로 만든 shared_ptr은 enable_shared_from_this를 초기화하지 않습니다. 따라서 Barenable_shared_from_this<Bar>를 상속하고, aliasing으로 만든 shared_ptr<Bar>에서 shared_from_this()를 호출하면 std::bad_weak_ptr이 발생할 수 있습니다. aliasing은 “수명만 공유”하고, 가리키는 객체의 enable_shared_from_this는 설정하지 않습니다.

사용 사례

사례설명
부모의 멤버 반환shared_ptr<Parent>로 자식 멤버의 수명 보장
배열 요소shared_ptr<int>(arr, &arr[i])로 i번째 요소 shared_ptr
내부 구현 노출Pimpl의 내부 포인터를 수명에 묶어 반환

4. 커스텀 삭제자 (Custom Deleter)

커스텀 삭제자란?

shared_ptr은 기본적으로 참조 카운트가 0이 될 때 delete ptr을 호출합니다. FILE*, int[], HANDLE, 소켓 등 delete가 아닌 다른 해제 방식이 필요한 리소스는 커스텀 삭제자를 지정해야 합니다. 삭제자는 void(T* ptr) 시그니처를 만족하는 함수, 람다, std::function 등 호출 가능 객체입니다.

FILE* 관리 (fclose)

#include <memory>
#include <cstdio>
#include <iostream>

int main() {
    // ✅ FILE*를 fclose로 해제하는 shared_ptr
    std::shared_ptr<FILE> file(
        fopen("test.txt", "w"),
         {
            if (fp) {
                std::cout << "fclose 호출\n";
                fclose(fp);
            }
        }
    );
    if (file) {
        fprintf(file.get(), "Hello, shared_ptr!\n");
    }
    return 0;  // 스코프 종료 시 람다가 fclose 호출
}

코드 설명:

  • shared_ptr<FILE>(fopen(...), lambda): 첫 인자는 포인터, 두 번째는 삭제자입니다.
  • 람다 { if (fp) fclose(fp); }가 참조 카운트 0 시 호출됩니다.
  • file.get()으로 raw 포인터를 얻어 fprintf에 전달합니다.

배열 관리 (delete[])

#include <memory>
#include <iostream>

int main() {
    // ✅ new[]로 할당한 배열 → delete[]로 해제
    std::shared_ptr<int> arr(
        new int[10],
         {
            std::cout << "delete[] 호출\n";
            delete[] p;
        }
    );
    for (int i = 0; i < 10; ++i) arr.get()[i] = i;
    return 0;
}

주의: std::make_shared는 배열을 지원하지 않습니다(C++17 이전). shared_ptr<int>(new int[10], std::default_delete<int[]>()) 또는 C++17의 std::shared_ptr<int[]>를 사용할 수 있습니다.

C++17 default_delete 활용

#include <memory>

// C++17: shared_ptr이 배열을 기본 지원
std::shared_ptr<int[]> arr(new int[100], std::default_delete<int[]>());

// 또는 make_shared 대신 allocate_shared (배열용)
// std::shared_ptr<int[]> arr = std::make_shared<int[]>(100);  // C++20

뮤텍스 잠금 해제 (RAII 스타일)

#include <memory>
#include <mutex>
#include <iostream>

void processWithLock(std::mutex& mtx) {
    auto lockPtr = std::shared_ptr<std::mutex>(
        &mtx,
         {
            if (m) {
                std::cout << "unlock\n";
                m->unlock();
            }
        }
    );
    mtx.lock();
    // ... 작업 ...
    // lockPtr 소멸 시 unlock 호출 (주의: mtx 수명이 lockPtr보다 길어야 함)
}

주의: 이 패턴은 mtxlockPtr보다 오래 살아 있어야 합니다. 일반적으로 std::unique_lock을 쓰는 것이 더 안전합니다. shared_ptr로 “수명 연장”이 필요할 때만 사용합니다.

삭제자 타입과 제어 블록

커스텀 삭제자를 쓰면 제어 블록에 삭제자가 저장됩니다. 삭제자마다 타입이 다르므로 shared_ptr<T>끼리는 호환되지만, make_shared와 달리 객체와 제어 블록이 분리되어 할당이 2번 발생할 수 있습니다. 성능이 중요하면 make_shared + 기본 delete를 우선 고려하세요.


5. shared_ptr 스레드 안전성

무엇이 스레드 안전한가?

flowchart TB
    subgraph safe["✅ 스레드 안전"]
        S1["참조 카운트 증감"]
        S2["서로 다른 shared_ptr 객체에서 복사/이동"]
        S3["객체 소멸 시점 동기화"]
    end
    subgraph unsafe["❌ 스레드 안전하지 않음"]
        U1["같은 shared_ptr 객체 동시 수정"]
        U2["get()으로 얻은 raw 포인터 동시 접근"]
    end

스레드 안전:

  • 참조 카운트의 증감은 원자적(atomic) 연산입니다.
  • 서로 다른 shared_ptr 객체(복사본)를 여러 스레드가 각각 사용하는 것은 안전합니다. 예: 스레드 A가 shared_ptr 복사본을 가지고, 스레드 B가 다른 복사본을 가질 때, 각 스레드가 자기 복사본만 수정하면 됩니다.

스레드 안전하지 않음:

  • 같은 shared_ptr 객체를 여러 스레드가 동시에 수정(reset, 대입 등)하면 데이터 레이스가 발생합니다.
  • get()으로 얻은 raw 포인터를 여러 스레드가 동시에 사용할 때, 그 객체 자체에 대한 동기화는 사용자가 책임져야 합니다.

안전한 패턴: 스레드마다 복사본

#include <memory>
#include <thread>
#include <vector>
#include <iostream>

void worker(std::shared_ptr<int> ptr) {
    // 각 스레드는 자기 복사본을 가짐 → 안전
    std::cout << "use_count: " << ptr.use_count() << std::endl;
}

int main() {
    auto ptr = std::make_shared<int>(42);
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(worker, ptr);  // 값으로 전달 → 복사
    }
    for (auto& t : threads) t.join();
    return 0;
}

코드 설명:

  • workershared_ptr값으로 전달하면 각 스레드가 자기 복사본을 받습니다.
  • 복사 시 참조 카운트만 원자적으로 증가하므로 스레드 안전합니다.
  • 각 스레드가 자기 복사본만 사용하므로 데이터 레이스가 없습니다.

위험한 패턴: 같은 shared_ptr 공유 수정

// ❌ 위험: 같은 ptr을 여러 스레드가 수정
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::thread t1([&ptr]() { ptr = std::make_shared<int>(1); });  // 데이터 레이스!
std::thread t2([&ptr]() { ptr.reset(); });                     // 데이터 레이스!
t1.join(); t2.join();

해결: 스레드마다 복사본을 두거나, std::atomic<std::shared_ptr<T>>(C++20)를 사용합니다.

atomic<shared_ptr> (C++20)

여러 스레드가 같은 변수에 shared_ptr을 교체하는 경우, std::atomic<std::shared_ptr<T>>(C++20)를 사용하면 데이터 레이스 없이 동기화할 수 있습니다. load, store, exchange, compare_exchange_strong 등이 모두 지원됩니다.

#include <atomic>
#include <memory>
#include <thread>
#include <iostream>

std::atomic<std::shared_ptr<int>> atomicPtr;

void writer() {
    for (int i = 0; i < 5; ++i) {
        atomicPtr.store(std::make_shared<int>(i));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void reader() {
    for (int i = 0; i < 5; ++i) {
        auto local = atomicPtr.load();  // 스레드 안전한 읽기
        if (local) {
            std::cout << "Reader: " << *local << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(15));
    }
}

int main() {
    atomicPtr.store(std::make_shared<int>(0));
    std::thread t1(writer);
    std::thread t2(reader);
    t1.join();
    t2.join();
    return 0;
}

코드 설명:

  • atomicPtr.store(): 다른 스레드가 안전하게 새 shared_ptr을 쓸 수 있습니다.
  • atomicPtr.load(): 현재 shared_ptr의 복사본을 원자적으로 가져옵니다.
  • 각 스레드가 local 복사본만 사용하므로 데이터 레이스가 없습니다.

compare_exchange로 원자적 교체

#include <atomic>
#include <memory>

std::atomic<std::shared_ptr<int>> atomicPtr;

void updateIfEqual(int expected, int newVal) {
    auto current = atomicPtr.load();
    while (current && *current == expected) {
        auto desired = std::make_shared<int>(newVal);
        if (atomicPtr.compare_exchange_strong(current, desired)) {
            break;  // 성공적으로 교체됨
        }
        // current가 compare_exchange에서 실패 시 최신 값으로 갱신됨
    }
}

참고: C++20 이전에는 std::atomic_load(&ptr), std::atomic_store(&ptr, value) 등을 사용합니다.

// C++17 이전
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::shared_ptr<int> loaded = std::atomic_load(&ptr);
std::atomic_store(&ptr, std::make_shared<int>(100));

요약 표

연산스레드 안전
복사 생성/대입 (서로 다른 객체)
참조 카운트 증감
같은 shared_ptr 객체 동시 수정
get() 반환 포인터로의 객체 접근사용자 동기화 필요

5. 완전한 예제 코드

예제 1: enable_shared_from_this + 비동기 타이머

#include <memory>
#include <functional>
#include <thread>
#include <chrono>
#include <iostream>

class TimedService : public std::enable_shared_from_this<TimedService> {
public:
    void scheduleCallback(int ms, std::function<void()> fn) {
        auto self = shared_from_this();
        std::thread([self, ms, fn]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(ms));
            if (fn) fn();
        }).detach();
    }
};

int main() {
    auto svc = std::make_shared<TimedService>();
    svc->scheduleCallback(50,  { std::cout << "Done\n"; });
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return 0;
}

예제 2: aliasing으로 컨테이너 멤버 반환

#include <memory>
#include <vector>
#include <iostream>

struct Node {
    int id;
    std::vector<int> data;
};

class NodeManager {
    std::shared_ptr<Node> root_;
public:
    NodeManager() : root_(std::make_shared<Node>()) {
        root_->id = 1;
        root_->data = {1, 2, 3};
    }
    std::shared_ptr<std::vector<int>> getData() {
        return std::shared_ptr<std::vector<int>>(root_, &root_->data);
    }
};

int main() {
    NodeManager mgr;
    auto dataPtr = mgr.getData();
    for (int x : *dataPtr) std::cout << x << " ";
    std::cout << std::endl;
    return 0;
}

예제 3: 스레드 풀에 shared_ptr 작업 전달

#include <memory>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <iostream>

class ThreadPool {
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool stop_ = false;
public:
    ThreadPool(size_t n) {
        for (size_t i = 0; i < n; ++i) {
            workers_.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lk(mtx_);
                        cv_.wait(lk, [this] { return stop_ || !tasks_.empty(); });
                        if (stop_ && tasks_.empty()) return;
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    task();
                }
            });
        }
    }
    template<typename F>
    void submit(F&& f) {
        {
            std::lock_guard<std::mutex> lk(mtx_);
            tasks_.emplace(std::forward<F>(f));
        }
        cv_.notify_one();
    }
    ~ThreadPool() {
        { std::lock_guard<std::mutex> lk(mtx_); stop_ = true; }
        cv_.notify_all();
        for (auto& w : workers_) w.join();
    }
};

int main() {
    ThreadPool pool(2);
    auto config = std::make_shared<int>(42);
    pool.submit([config]() {
        std::cout << "Task: " << *config << std::endl;
    });
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return 0;
}

예제 4: enable_shared_from_this + aliasing 조합

#include <memory>
#include <iostream>

struct Payload { int value = 0; };

class Handler : public std::enable_shared_from_this<Handler> {
    Payload payload_;
public:
    Handler(int v) { payload_.value = v; }
    std::shared_ptr<Payload> getPayload() {
        return std::shared_ptr<Payload>(shared_from_this(), &payload_);
    }
};

int main() {
    auto handler = std::make_shared<Handler>(100);
    auto payload = handler->getPayload();
    std::cout << payload->value << std::endl;  // 100
    handler.reset();
    std::cout << payload->value << std::endl;  // 100 (payload가 수명 유지)
    return 0;
}

예제 5: 커스텀 삭제자 (FILE*, 배열)

#include <memory>
#include <cstdio>
#include <iostream>

int main() {
    // FILE*: fclose로 해제
    auto file = std::shared_ptr<FILE>(
        fopen("log.txt", "w"),
         { if (fp) fclose(fp); }
    );
    if (file) fprintf(file.get(), "Log entry\n");

    // 배열: delete[]로 해제
    auto arr = std::shared_ptr<int>(
        new int[5]{1, 2, 3, 4, 5},
         { delete[] p; }
    );
    for (int i = 0; i < 5; ++i) std::cout << arr.get()[i] << " ";
    std::cout << std::endl;
    return 0;
}

예제 6: weak_ptr 옵저버 패턴

#include <memory>
#include <vector>
#include <functional>
#include <iostream>

class Observer;

class Subject : public std::enable_shared_from_this<Subject> {
    std::vector<std::weak_ptr<Observer>> observers_;
public:
    void attach(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);  // weak_ptr로 저장 (참조 카운트 증가 안 함)
    }
    void notify() {
        for (auto& wp : observers_) {
            auto sp = wp.lock();
            if (sp) {
                sp->onNotify();
            }
            // expired면 무시 (이미 삭제된 구독자)
        }
    }
};

class Observer : public std::enable_shared_from_this<Observer> {
    int id_;
public:
    Observer(int id) : id_(id) {}
    void onNotify() {
        std::cout << "Observer " << id_ << " notified\n";
    }
};

int main() {
    auto subject = std::make_shared<Subject>();
    auto obs1 = std::make_shared<Observer>(1);
    auto obs2 = std::make_shared<Observer>(2);

    subject->attach(obs1);
    subject->attach(obs2);
    subject->notify();  // Observer 1, 2 notified

    obs1.reset();  // obs1 해제
    subject->notify();  // Observer 2만 notified (obs1은 expired)
    return 0;
}

코드 설명:

  • Subjectvector<weak_ptr<Observer>>로 구독자를 저장합니다. shared_ptr이 아니므로 참조 카운트를 올리지 않아 구독자가 스스로 해제될 수 있습니다.
  • notify()에서 lock()으로 shared_ptr을 얻고, 유효하면 콜백을 호출합니다. expired()인 구독자는 건너뜁니다.
  • 이 패턴으로 “있으면 알리고, 없으면 무시”하는 옵저버 패턴을 구현할 수 있습니다.

7. 자주 발생하는 에러와 해결법

에러 1: shared_ptr(this)로 이중 해제

증상: 프로그램 크래시 (double-free 또는 corruption)

// ❌ 잘못된 코드
class Bad {
public:
    std::shared_ptr<Bad> getPtr() {
        return std::shared_ptr<Bad>(this);
    }
};
auto p1 = std::make_shared<Bad>();
auto p2 = p1->getPtr();  // 서로 다른 제어 블록 → 이중 해제

해결법:

// ✅ 올바른 코드
class Good : public std::enable_shared_from_this<Good> {
public:
    std::shared_ptr<Good> getPtr() {
        return shared_from_this();
    }
};

에러 2: shared_from_this()를 스택 객체에서 호출

증상: std::bad_weak_ptr 예외

// ❌ 잘못된 코드
class Service : public std::enable_shared_from_this<Service> {};
Service svc;  // 스택에 생성
auto p = svc.shared_from_this();  // bad_weak_ptr!

해결법:

// ✅ 올바른 코드: 반드시 shared_ptr로 생성
auto svc = std::make_shared<Service>();
auto p = svc->shared_from_this();

에러 3: 생성자에서 shared_from_this() 호출

증상: std::bad_weak_ptr 예외

// ❌ 잘못된 코드
class Bad : public std::enable_shared_from_this<Bad> {
public:
    Bad() {
        auto p = shared_from_this();  // 아직 shared_ptr이 없음!
    }
};

해결법:

// ✅ 올바른 코드: 생성자 밖에서 호출
class Good : public std::enable_shared_from_this<Good> {
public:
    void init() {
        auto p = shared_from_this();  // init()은 생성 후 호출
    }
};
auto obj = std::make_shared<Good>();
obj->init();

에러 4: 멤버 포인터를 독립 shared_ptr로 생성

증상: use-after-free (부모가 먼저 삭제됨)

// ❌ 잘못된 코드
auto container = std::make_shared<Container>();
std::shared_ptr<Bar> barPtr(container->member);  // 잘못된 사용
// 또는
std::shared_ptr<Bar> barPtr(&container->member);  // container와 무관한 제어 블록!

해결법:

// ✅ 올바른 코드: aliasing 생성자
std::shared_ptr<Bar> barPtr(container, &container->member);

에러 5: 같은 shared_ptr을 여러 스레드가 동시 수정

증상: 데이터 레이스, 정의되지 않은 동작

// ❌ 잘못된 코드
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::thread t1([&ptr]() { ptr.reset(); });
std::thread t2([&ptr]() { ptr = std::make_shared<int>(1); });

해결법:

// ✅ 올바른 코드: 스레드마다 복사본
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::thread t1([ptr]() { /* ptr 복사본 사용 */ });
std::thread t2([ptr]() { /* ptr 복사본 사용 */ });

에러 6: 비동기 콜백에 this 직접 캡처

증상: use-after-free

// ❌ 잘못된 코드
void startAsync() {
    asyncCall([this]() {
        this->onComplete();  // this가 이미 삭제됐을 수 있음!
    });
}

해결법:

// ✅ 올바른 코드
void startAsync() {
    auto self = shared_from_this();
    asyncCall([self]() {
        self->onComplete();
    });
}

에러 7: 배열에 delete 사용 (delete[] 누락)

증상: 미정의 동작, 힙 손상

// ❌ 잘못된 코드
std::shared_ptr<int> arr(new int[10]);  // 기본 delete 사용 → delete[] 아님!

해결법:

// ✅ 올바른 코드: 커스텀 삭제자로 delete[]
std::shared_ptr<int> arr(new int[10],  { delete[] p; });
// 또는 C++17
std::shared_ptr<int[]> arr(new int[10], std::default_delete<int[]>());

에러 8: weak_ptr lock() 결과 검사 누락

증상: null 역참조, use-after-free

// ❌ 잘못된 코드
void callback(std::weak_ptr<Service> wp) {
    wp.lock()->doWork();  // lock()이 nullptr 반환하면 크래시!
}

해결법:

// ✅ 올바른 코드: lock() 결과 검사
void callback(std::weak_ptr<Service> wp) {
    auto sp = wp.lock();
    if (sp) {
        sp->doWork();
    }
}

에러 9: 커스텀 삭제자에서 예외 발생

증상: 참조 카운트 0 시 삭제자가 호출되는데, 삭제자에서 예외를 던지면 std::terminate 호출

// ❌ 위험: 삭제자에서 예외
std::shared_ptr<Resource> r(ptr,  {
    if (!p->close()) throw std::runtime_error("close failed");  // terminate!
});

해결법:

// ✅ 올바른 코드: 삭제자에서는 예외를 던지지 않음
std::shared_ptr<Resource> r(ptr,  {
    if (p) {
        try {
            p->close();
        } catch (...) {
            std::cerr << "close failed\n";  // 로깅만, 예외 전파 금지
        }
    }
});

8. 모범 사례

선택 플로우차트

flowchart TD
    A["this를 shared_ptr로 넘겨야 하나?"] -->|Yes| B["enable_shared_from_this 상속"]
    A -->|No| C["멤버 포인터 수명을 부모에 묶어야 하나?"]
    C -->|Yes| D["aliasing 생성자 사용"]
    C -->|No| E["일반 shared_ptr 사용"]
    B --> F["shared_from_this() 사용"]
    F --> G["반드시 make_shared로 생성"]

모범 사례 요약

규칙설명
this → shared_ptr 필요 시enable_shared_from_this + shared_from_this()
shared_from_this 사용 클래스반드시 make_shared로 생성
생성자/소멸자shared_from_this() 호출 금지
부모 수명에 묶인 멤버aliasing 생성자 shared_ptr(r, ptr)
멀티스레드스레드마다 shared_ptr 복사본, 또는 atomic
비동기 콜백this 대신 shared_from_this() 캡처
FILE*/배열 등커스텀 삭제자 지정
옵저버 패턴발행자는 weak_ptr로 구독자 저장
삭제자예외 던지지 않기
weak_ptr 사용 시lock() 결과 반드시 검사

enable_shared_from_this 체크리스트

  • 클래스가 enable_shared_from_thispublic 상속하는가?
  • 객체를 항상 make_shared 또는 shared_ptr(new T)로 생성하는가?
  • shared_from_this()를 생성자/소멸자에서 호출하지 않는가?
  • 비동기 콜백에 this 대신 shared_from_this()를 넘기는가?

aliasing 체크리스트

  • ptrr이 관리하는 객체의 일부(멤버, 배열 요소 등)인가?
  • ptr의 수명이 r에 묶여 있어야 하는 상황인가?
  • aliasing으로 만든 shared_ptr에서 shared_from_this()를 호출하지 않는가? (해당 객체가 enable_shared_from_this를 상속한 경우)

커스텀 삭제자 체크리스트

  • delete가 아닌 해제 방식이 필요한가? (FILE*, 배열, HANDLE 등)
  • 삭제자에서 예외를 던지지 않는가?
  • 배열의 경우 delete[] 또는 default_delete<T[]>를 사용하는가?

weak_ptr 옵저버 체크리스트

  • 발행자가 구독자 수명을 소유하지 않아도 되는가?
  • lock() 호출 후 반드시 null 검사를 하는가?
  • expired()인 구독자는 안전하게 건너뛰는가?

9. 프로덕션 패턴

패턴 1: 네트워크 세션 (enable_shared_from_this)

class Session : public std::enable_shared_from_this<Session> {
    asio::ip::tcp::socket socket_;
public:
    void asyncRead() {
        auto self = shared_from_this();
        socket_.async_read_some(buffer_, [self](error_code ec, size_t len) {
            if (!ec) self->onRead(len);
        });
    }
};
// Session은 make_shared로 생성

패턴 2: 이벤트 구독자 (shared_from_this)

class Subscriber : public std::enable_shared_from_this<Subscriber> {
public:
    void subscribe(EventBus& bus) {
        bus.add(shared_from_this());
    }
};

패턴 3: 캐시에서 멤버 반환 (aliasing)

std::shared_ptr<Config> Cache::getConfig(const std::string& key) {
    auto entry = getEntry(key);  // shared_ptr<CacheEntry>
    return std::shared_ptr<Config>(entry, &entry->config_);
}

패턴 4: 스레드 풀 작업 (shared_ptr 캡처)

void submitTask(std::shared_ptr<Context> ctx) {
    pool.submit([ctx]() {
        process(ctx);  // ctx 수명이 작업 완료까지 유지
    });
}

패턴 5: 팩토리 + enable_shared_from_this

class Service : public std::enable_shared_from_this<Service> {
    Service() = default;  // private
public:
    static std::shared_ptr<Service> create() {
        return std::shared_ptr<Service>(new Service());
    }
};

패턴 6: 파일 핸들 RAII (커스텀 삭제자)

using FileHandle = std::shared_ptr<FILE>;

FileHandle openFile(const char* path, const char* mode) {
    FILE* fp = fopen(path, mode);
    if (!fp) return nullptr;
    return FileHandle(fp,  { if (f) fclose(f); });
}

void processFile(FileHandle f) {
    if (f) fprintf(f.get(), "data\n");
}  // 여러 함수가 FileHandle을 공유해도, 마지막 소유자가 없어지면 fclose

패턴 7: weak_ptr 옵저버 (이벤트 버스)

class EventBus {
    std::vector<std::weak_ptr<EventListener>> listeners_;
public:
    void subscribe(std::shared_ptr<EventListener> l) {
        listeners_.push_back(l);
    }
    void publish(const Event& e) {
        for (auto it = listeners_.begin(); it != listeners_.end(); ) {
            auto sp = it->lock();
            if (sp) {
                sp->onEvent(e);
                ++it;
            } else {
                it = listeners_.erase(it);  // 만료된 구독자 제거
            }
        }
    }
};

패턴 8: atomic<shared_ptr>로 동적 설정 교체

std::atomic<std::shared_ptr<Config>> globalConfig;

void reloadConfig() {
    auto newConfig = std::make_shared<Config>(loadFromFile());
    globalConfig.store(newConfig);  // 원자적 교체
}

void worker() {
    auto config = globalConfig.load();  // 항상 최신 또는 이전 버전 (안전)
    if (config) config->apply();
}

10. 성능과 체크리스트

enable_shared_from_this 오버헤드

  • enable_shared_from_this는 내부에 weak_ptr을 하나 추가합니다.
  • 객체 크기가 약간 증가하고, make_shared 시 제어 블록에 weak_ptr 관련 공간이 필요합니다.
  • shared_from_this() 호출 시 weak_ptr을 shared_ptr로 변환하는 비용이 있으나, 일반적으로 무시할 수준입니다.

aliasing 생성자 오버헤드

  • aliasing 생성자는 새 제어 블록을 만들지 않습니다. 기존 shared_ptr의 제어 블록을 공유합니다.
  • 참조 카운트만 증가하므로 오버헤드가 적습니다.

프로덕션 체크리스트

  • enable_shared_from_this 사용 시 make_shared로 생성
  • 생성자/소멸자에서 shared_from_this() 호출 금지
  • 멤버 포인터 반환 시 aliasing 생성자 사용
  • 멀티스레드에서 shared_ptr 공유 시 스레드마다 복사본 또는 atomic
  • 비동기 콜백에 this 대신 shared_from_this() 전달

마무리

핵심 요약

enable_shared_from_this

  • this를 안전하게 shared_ptr로 변환
  • shared_ptr(this)는 이중 해제 유발 → 사용 금지
  • 반드시 make_shared로 생성, 생성자/소멸자에서 호출 금지

aliasing 생성자

  • shared_ptr<T>(r, ptr): r의 수명에 ptr을 묶음
  • 부모 멤버를 shared_ptr로 반환할 때 사용

커스텀 삭제자

  • FILE*, 배열, HANDLE 등 delete가 아닌 리소스용
  • shared_ptr<T>(ptr, deleter) 형태, 삭제자에서 예외 금지

atomic<shared_ptr> (C++20)

  • 같은 변수에 여러 스레드가 shared_ptr 교체 시 사용
  • load, store, compare_exchange_strong 지원

weak_ptr 옵저버 패턴

  • 발행자는 weak_ptr로 구독자 저장, lock()으로 접근
  • lock() 결과 반드시 검사

스레드 안전성

  • 참조 카운트는 원자적
  • 같은 shared_ptr 객체 동시 수정은 데이터 레이스
  • 스레드마다 복사본 보유 권장

다음 글

weak_ptr순환 참조 해결은 C++ 스마트 포인터와 순환 참조 해결법을 참고하세요.


자주 묻는 질문 (FAQ)

Q. shared_from_this()를 생성자에서 호출할 수 없나요?

A. 불가능합니다. 생성자가 실행되는 시점에는 아직 shared_ptr이 객체를 소유하지 않아, 내부 weak_ptr이 초기화되지 않습니다. 초기화 로직이 필요하면 void init() 같은 메서드를 두고, make_shared로 생성한 뒤 init()을 호출하세요.

Q. aliasing으로 만든 shared_ptr에서 shared_from_this()를 쓸 수 있나요?

A. 가리키는 객체(ptr)가 enable_shared_from_this를 상속했고, 그 객체가 shared_ptr로 직접 생성된 경우에만 가능합니다. aliasing 생성자는 enable_shared_from_this를 초기화하지 않으므로, aliasing으로만 만들어진 shared_ptr에서는 shared_from_this()가 실패할 수 있습니다.

Q. shared_ptr을 함수 인자로 넘길 때 값으로 받나요, const 참조로 받나요?

A. 함수가 소유권을 공유해야 하면 값으로 받는 것이 좋습니다. 복사가 발생하지만 참조 카운트만 원자적으로 증가하며, 호출자가 std::move로 넘길 수도 있습니다. 읽기만 하고 소유권을 늘리지 않을 때는 const shared_ptr<T>&를 사용합니다.

Q. 커스텀 삭제자를 쓸 때 make_shared를 사용할 수 있나요?

A. make_shared는 커스텀 삭제자를 지원하지 않습니다. shared_ptr<T>(new T(...), deleter) 형태로 생성해야 합니다. 이 경우 객체와 제어 블록이 분리되어 할당이 2번 발생할 수 있어, make_shared보다 약간의 오버헤드가 있습니다.

Q. 옵저버 패턴에서 shared_ptr 대신 weak_ptr을 쓰는 이유는?

A. 발행자가 shared_ptr로 구독자를 들면, 구독자가 자기 자신을 해제해도 발행자가 소유하고 있어 객체가 영원히 살아 있습니다. weak_ptr은 참조 카운트를 올리지 않으므로, 구독자가 스스로 해제될 수 있고, 발행자는 lock()으로 “있으면 쓰고, 없으면 무시”할 수 있습니다.


참고 자료


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

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

  • C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
  • C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
  • C++ RAII | “파일을 열 수 없습니다” 장애의 원인과 자동 리소스 관리

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

C++, shared_ptr, enable_shared_from_this, aliasing, weak_ptr, custom_deleter, atomic_shared_ptr, 스레드안전성, 스마트포인터, 메모리관리 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
  • C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
  • C++ Google Mock |
  • C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열
  • C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)