← 문제로

21. std::enable_shared_from_this 와 shared_from_this() 의 내부 동작

난이도 중
내 리뷰 · C++
해설 · C++

해설 — std::enable_shared_from_this 와 shared_from_this() 의 내부 동작

난이도: 중상

요약

enable_shared_from_this<T> 는 기저 클래스 안에 자신을 가리키는 weak_ptr<T> 를 숨겨 둔다. 객체를 처음 shared_ptr 로 소유하는 순간 그 제어블록이 이 weak_ptr 에 심어지고, 이후 shared_from_this() 는 그 weak_ptrlock()기존 제어블록을 공유하는shared_ptr 을 돌려준다. 그래서 shared_ptr 소유가 성립하기 전(스택 객체·생성자)에는 심긴 게 없어 실패한다. 예상 출력:

1: 1            // make_shared 직후 use_count
2: 2            // self() 가 소유권을 하나 더 만듦
3: 1            // 같은 객체를 가리킴
4: 1            // weak_from_this().lock() 도 같은 객체
5: bad_weak_ptr // 스택 객체에서 호출 → 예외

핵심 개념

  • enable_shared_from_this<T> 는 대략 이런 멤버를 갖는다: mutable weak_ptr<T> __weak_this;.
  • 핵심 불변식: 이 weak_ptr 는 객체가 shared_ptr 로 소유될 때에만 유효하다. shared_ptr 생성자가 "대상이 enable_shared_from_this 를 상속했는가" 를 감지해 __weak_this 를 자신의 제어블록으로 초기화한다(enable 훅).
  • shared_from_this() = shared_ptr<T>(__weak_this) 이며, 이는 새 제어블록을 만들지 않고 기존 제어블록의 강한 참조 카운트를 +1 한다.

단계별 내부 동작

  1. make_shared<Session>(1) (라인 1): 객체와 제어블록을 한 덩어리로 할당하고, Sessionenable_shared_from_this 를 상속했음을 감지해 __weak_this 에 이 제어블록을 심는다. 강한 카운트 1 → use_count()==1.
  2. sp->self()shared_from_this() (라인 2,3): __weak_this.lock() 이 성공해 같은 제어블록을 공유하는 shared_ptr 생성 → 강한 카운트 1→2. s2sp 는 같은 객체(get() 동일)이자 같은 제어블록(카운트를 공유). 새 제어블록을 따로 만들면 두 개의 독립 카운트가 같은 객체를 이중 해제하게 되는데, enable_shared_from_this 는 바로 이 실수를 막아 준다.
  3. weak_from_this() (라인 4): __weak_this 를 그대로 돌려주는 셈. lock() 하면 살아 있는 동안 같은 객체를 얻는다(C++17 추가 API).
  4. 스택 객체에서 호출 (라인 5): Session local(2)shared_ptr 로 소유된 적이 없어 __weak_this 가 비어 있다(expired). shared_from_this()shared_ptr(weak) 생성이 만료된 weak_ptr 를 만나 std::bad_weak_ptr 를 던진다(C++17 규정; C++11 이전 일부 구현은 UB 였음).

흔한 오해·함정

  • 생성자에서 shared_from_this() 호출 → 아직 shared_ptr 소유가 성립하기 전이라 __weak_this 가 비어 있음 → bad_weak_ptr. 초기화가 필요하면 팩토리(make_shared 후 별도 init())로 미룬다.
  • 객체를 new 로 만들고 raw 포인터로만 쓰다가 shared_from_this() → 소유 shared_ptr 이 없어 실패. 반드시 먼저 shared_ptr(가급적 make_shared)로 소유.
  • 같은 객체에 두 개의 독립 shared_ptr(서로 다른 제어블록) → 이중 해제. enable_shared_from_this 를 쓰거나, 소유 shared_ptr 을 넘겨 다니면 방지. (제어블록의 수명·순환 참조 자체는 problem3 을 참조 — 이 문제는 그 위에서 shared_from_this 훅 메커니즘에 초점.)
  • enable_shared_from_this 다중/가상 상속__weak_this 가 모호해질 수 있으니 상속 그래프에서 한 번만 등장하도록 설계.
  • 수명 연장 오용: 비동기 콜백에 shared_from_this() 를 캡처하면 콜백이 사는 동안 객체가 안 죽는다(안전). 반대로 콜백을 취소하지 않으면 참조 순환/좀비가 될 수 있으니 취소 경로도 설계.

면접 포인트

  1. enable_shared_from_this 가 내부 weak_ptr 로 동작하는 원리, 그리고 그것이 언제(첫 shared_ptr 소유 시) 채워지는지.
  2. shared_from_this() 가 새 제어블록을 만들지 않고 기존 카운트를 공유해 이중 해제를 막는가.
  3. 게임 서버 비동기 I/O 패턴: 완료 핸들러에 shared_from_this() 를 실어 처리 중 세션 수명을 보장하는 관용구와, 스택 객체/생성자 호출 함정(bad_weak_ptr).