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_ptr 를 lock() 해 기존 제어블록을 공유하는 새 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 한다.
단계별 내부 동작
make_shared<Session>(1)(라인 1): 객체와 제어블록을 한 덩어리로 할당하고,Session이enable_shared_from_this를 상속했음을 감지해__weak_this에 이 제어블록을 심는다. 강한 카운트 1 →use_count()==1.sp->self()→shared_from_this()(라인 2,3):__weak_this.lock()이 성공해 같은 제어블록을 공유하는shared_ptr생성 → 강한 카운트 1→2.s2와sp는 같은 객체(get()동일)이자 같은 제어블록(카운트를 공유). 새 제어블록을 따로 만들면 두 개의 독립 카운트가 같은 객체를 이중 해제하게 되는데,enable_shared_from_this는 바로 이 실수를 막아 준다.weak_from_this()(라인 4):__weak_this를 그대로 돌려주는 셈.lock()하면 살아 있는 동안 같은 객체를 얻는다(C++17 추가 API).- 스택 객체에서 호출 (라인 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()를 캡처하면 콜백이 사는 동안 객체가 안 죽는다(안전). 반대로 콜백을 취소하지 않으면 참조 순환/좀비가 될 수 있으니 취소 경로도 설계.
면접 포인트
enable_shared_from_this가 내부weak_ptr로 동작하는 원리, 그리고 그것이 언제(첫 shared_ptr 소유 시) 채워지는지.- 왜
shared_from_this()가 새 제어블록을 만들지 않고 기존 카운트를 공유해 이중 해제를 막는가. - 게임 서버 비동기 I/O 패턴: 완료 핸들러에
shared_from_this()를 실어 처리 중 세션 수명을 보장하는 관용구와, 스택 객체/생성자 호출 함정(bad_weak_ptr).