C++ enable_shared_from_this 완벽 정리 | shared_ptr(this)가 위험한 이유

//
//

🚀 들어가며

이전 글에서는 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

C++ enable_shared_from_this와 shared_ptr(this)의 위험성을 설명하는 Control Block 구조 다이어그램
shared_ptr(this)는 새로운 Control Block을 생성할 수 있으며, enable_shared_from_this는 기존 Control Block을 안전하게 공유하기 위해 사용됩니다.

 

이 문제를 해결하기 위해 등장한 것이:

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 개념을 이해하기 시작하면:

👉 왜 이런 클래스가 필요한지 자연스럽게 이해할 수 있게 됩니다.

 

스마트 포인터를 잘 사용한다는 것은 단순히 문법을 아는 것이 아니라,
객체의 관계와 생명주기를 설계할 수 있다는 뜻입니다.

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다