🚀 들어가며
이전 글에서는 weak_ptr이 왜 필요한지와 함께,
shared_ptr의 순환 참조 문제를 어떻게 해결하는지 정리해보았습니다.
shared_ptr은 매우 편리한 스마트 포인터입니다.
객체의 생명주기를 자동으로 관리해주고,
객체의 소멸 시점을 정확하게 알 수 없는 상황에서도 안전하게 객체를 공유할 수 있습니다.
하지만 shared_ptr을 사용할 때 주의해야 할 문제가 하나 더 있습니다.
바로:
👉 std::shared_ptr<T>(this)
입니다.
겉보기에는 단순히 현재 객체(this)를 shared_ptr로 감싸는 코드처럼 보입니다.
하지만 이 코드는 매우 위험할 수 있습니다.
심한 경우:
- Double Delete
- Heap Corruption
- 프로그램 크래시
같은 문제로 이어질 수 있습니다.
그리고 이 문제를 해결하기 위해 등장한 것이 바로
👉 std::enable_shared_from_this
입니다.
이번 글에서는:
- 왜 shared_ptr(this)가 위험한지
- Control Block이 왜 중복 생성되는지
- enable_shared_from_this가 이를 어떻게 해결하는지
를 자세하게 살펴보겠습니다.
🧠 shared_ptr의 핵심 다시 복습
이전 글에서 shared_ptr의 핵심 구조를 아래와 같이 정리했습니다.
shared_ptr
├── 실제 객체 포인터
└── Control Block
├── strong count
├── weak count
└── 삭제 정보
여기서 중요한 핵심은 이것입니다.
👉 shared_ptr의 소유권은 Control Block 기준으로 관리됩니다.
즉:
std::shared_ptr<Actor> actor = std::make_shared<Actor>();
이 코드가 실행되면:
Actor 객체 생성 ↓ Control Block 생성 ↓ shared_ptr가 Control Block 공유
구조가 만들어집니다.
그리고 shared_ptr가 복사될 때마다:
Control Block의 strong count 증가
가 발생합니다.
즉 shared_ptr는 단순히 포인터 주소를 공유하는 것이 아닙니다.
👉 동일한 Control Block을 공유하는 것이 핵심입니다.
⚠️ 문제의 시작
이제 아래 코드를 보겠습니다.
class Actor
{
public:
void Register()
{
manager->Add(std::shared_ptr<Actor>(this));
}
};
처음 보면 별 문제 없어 보입니다.
현재 객체(this)를 shared_ptr로 만들어 전달하는 코드처럼 보입니다.
하지만 실제로는 매우 위험한 코드입니다.
왜냐하면:
👉 새로운 Control Block이 생성되기 때문입니다.
💥 실제로 내부에서는 어떤 일이 발생할까?
예를 들어 아래와 같은 상황을 생각해보겠습니다.
std::shared_ptr<Actor> actor = std::make_shared<Actor>(); actor->Register();
현재 상태는 다음과 같습니다.
shared_ptr(actor)
└── Control Block A
└── Actor 객체
여기까지는 정상입니다.
그런데 Register 내부에서:
std::shared_ptr<Actor>(this)
를 호출하는 순간 문제가 발생합니다.
shared_ptr는 단순히 주소를 공유하지 않습니다.
새로운 shared_ptr를 생성하면:
👉 새로운 Control Block을 생성합니다.
즉 내부적으로는 이런 상태가 됩니다.
shared_ptr(actor)
└── Control Block A
└── Actor 객체
shared_ptr(this)
└── Control Block B
└── 같은 Actor 객체
중요한 점은:
👉 객체는 하나인데 Control Block은 두 개라는 것입니다.
💀 왜 위험할까?
shared_ptr는 자신이 관리하는 Control Block 기준으로 객체를 삭제합니다.
즉:
Control Block A → "내가 객체를 소유한다" Control Block B → "나도 객체를 소유한다"
상태가 됩니다.
결국 두 shared_ptr가 모두 소멸되면:
delete Actor delete Actor
가 두 번 발생할 수 있습니다.
즉:
👉 Double Delete 문제
가 발생합니다.
이는:
- 프로그램 크래시
- 힙 손상
- 예측 불가능한 동작
으로 이어질 수 있습니다.
🎯 핵심 문제 정리
여기서 핵심은 이것입니다.
std::shared_ptr<T>(this)
는:
👉 기존 shared_ptr의 소유권에 참여하는 것이 아닙니다.
대신:
👉 새로운 소유권 시스템(Control Block)을 생성합니다.
이 차이가 매우 중요합니다.
🧩 그래서 등장한 enable_shared_from_this

이 문제를 해결하기 위해 등장한 것이:
std::enable_shared_from_this
입니다.
사용 방법은 다음과 같습니다.
class Actor : public std::enable_shared_from_this<Actor>
{
public:
void Register()
{
std::shared_ptr<Actor> self = shared_from_this();
manager->Add(self);
}
};
겉보기에는 단순해 보입니다.
하지만 내부에서는 굉장히 중요한 작업이 일어납니다.
🔍 shared_from_this()는 무엇을 하는가?
shared_from_this()의 핵심 목적은 이것입니다.
👉 새로운 Control Block을 만들지 않는다.
대신:
👉 이미 존재하는 Control Block을 공유한다.
즉:
기존 shared_ptr ↓ 기존 Control Block 접근 ↓ strong count 증가 ↓ 새 shared_ptr 반환
과정을 수행합니다.
따라서 구조적으로는 다음과 같습니다.
shared_ptr A
└── Control Block
└── Actor 객체
shared_ptr B
└── 같은 Control Block 공유
즉 정상적인 shared_ptr 복사와 동일한 상태가 됩니다.
🧠 내부적으로 어떻게 가능할까?
많은 분들이 여기서 궁금해합니다.
"객체 내부에서 어떻게 기존 Control Block을 알 수 있을까?"
핵심은:
👉 enable_shared_from_this 내부에 weak_ptr가 존재한다는 점입니다.
개념적으로는 아래와 비슷한 구조입니다.
template<typename T>
class enable_shared_from_this
{
protected:
std::weak_ptr<T> weak_this;
};
shared_ptr가 객체를 최초 생성할 때:
현재 Control Block 정보를 객체 내부 weak_this에 저장
하게 됩니다.
그리고 이후:
shared_from_this()
를 호출하면:
weak_this.lock()
을 수행해 기존 Control Block을 공유하는 shared_ptr를 생성합니다.
즉:
👉 weak_ptr 기반으로 기존 소유권 시스템에 안전하게 참여하는 구조입니다.
⚠️ 주의해야 할 점
enable_shared_from_this에도 중요한 주의사항이 있습니다.
아래 코드는 위험합니다.
Actor actor; actor.shared_from_this();
왜냐하면 아직 shared_ptr가 객체를 관리하고 있지 않기 때문입니다.
즉:
Control Block 자체가 아직 없음
상태입니다.
따라서 shared_from_this()는:
👉 반드시 shared_ptr로 생성된 객체에서만 사용해야 합니다.
정상적인 사용 예시는 다음과 같습니다.
std::shared_ptr<Actor> actor = std::make_shared<Actor>(); actor->shared_from_this();
🎮 게임 개발에서는 왜 중요할까?
게임 개발에서는 객체가 자기 자신을 외부 시스템에 등록하는 경우가 많습니다.
예를 들면:
- 이벤트 시스템
- 델리게이트
- 비동기 작업
- Task 시스템
- Timer 시스템
등입니다.
이때 객체 자신의 shared_ptr를 안전하게 전달해야 하는 상황이 자주 발생합니다.
그래서 enable_shared_from_this는:
- 엔진 코드
- 비동기 시스템
- 네트워크 시스템
- Task 시스템
등에서 매우 자주 등장합니다.
특히:
- Boost.Asio
- 비동기 콜백 시스템
- Actor 기반 구조
에서는 거의 필수 수준으로 사용됩니다.
🧠 마무리
이번 글의 핵심은 이것입니다.
std::shared_ptr<T>(this) → 매우 위험할 수 있다
왜냐하면:
👉 새로운 Control Block을 생성하기 때문입니다.
그리고 이 문제를 해결하기 위해:
std::enable_shared_from_this
가 존재합니다.
핵심 원리는:
- 객체 내부 weak_ptr 저장
- 기존 Control Block 공유
- 새로운 shared_ptr 안전 생성
입니다.
shared_ptr를 단순히 “자동 메모리 관리 도구” 수준으로만 이해하면 이런 문제를 놓치기 쉽습니다.
하지만 내부 구조와 Control Block 개념을 이해하기 시작하면:
👉 왜 이런 클래스가 필요한지 자연스럽게 이해할 수 있게 됩니다.
스마트 포인터를 잘 사용한다는 것은 단순히 문법을 아는 것이 아니라,
객체의 관계와 생명주기를 설계할 수 있다는 뜻입니다.