← 문제로

3. shared_ptr 제어블록, 순환 참조, weak_ptr

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

해설 — shared_ptr 제어블록, 순환 참조, weak_ptr

난이도: 상

답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트

요약

예상 출력:

ctor A
a.use_count=1
after weak, a.use_count=1
in block, a.use_count=2
after block, a.use_count=1
ctor X
ctor Y
x.use_count=2, y.use_count=2
x.use_count after shared_from_this=3
end of main
dtor A

핵심: dtor Xdtor Y 는 출력되지 않는다(누수). xy 가 서로를 next(강한 참조)로 가리켜 참조계수가 서로 1씩 받쳐주기 때문이다. weak_ptr w 는 강한 계수를 올리지 않아 a.use_count 가 1 그대로다. shared_from_this() 는 같은 제어블록을 공유하는 새 shared_ptr 를 만들어 x 의 강한 계수를 2→3 으로 올린다.

핵심 개념

  • 제어블록(control block): shared_ptr 가 가리키는 객체와 별도로 존재하는 메타데이터. ① strong count(use_count): 객체 수명 관리, ② weak count: 제어블록 수명 관리. strong→0 이면 객체 파괴, strong=0 이고 weak=0 이면 제어블록 해제.
  • make_shared: 객체 + 제어블록을 한 번의 할당으로 묶는다(메모리 1개, 지역성↑). shared_ptr<T>(new T) 는 두 번 할당.
  • 원자적 참조계수: strong/weak 증감은 원자적(스레드 안전). 단 "가리키는 객체의 내용"은 보호하지 않는다(데이터 레이스는 별개). 또 atomic 증감 비용이 있으니 핫패스에서는 불필요한 복사(계수 증감)를 줄여야 한다.
  • weak_ptr: 강한 계수를 올리지 않는 관찰자. lock() 으로 살아있으면 shared_ptr 승격, 죽었으면 빈 shared_ptr. 순환을 끊는 도구.
  • enable_shared_from_this: 객체가 자신을 소유하는 제어블록을 알고 있어 shared_from_this()같은 제어블록의 shared_ptr 를 만든다(주의: 반드시 이미 shared_ptr 로 소유된 상태여야).

단계별 내부 동작

  1. make_shared<Node>("A") → 객체+제어블록 한 번에. a.use_count=1.
  2. weak_ptr w = a → weak count 만 증가, strong 그대로 1.
  3. 블록 내 b = a → strong 2. 블록 종료로 b 소멸 → strong 1.
  4. x, y 생성(각 strong 1). x->next = y(y strong 2), y->next = x(x strong 2). 출력 x.use_count=2, y.use_count=2.
  5. self = x->shared_from_this() → x 제어블록 공유, x strong 3.
  6. self.reset() → x strong 2.
  7. main 종료, 지역 역순 소멸(self→y→x→w→a):
    • 지역 y 소멸 → y strong 2→1(아직 x->next 가 가리킴). 파괴 안 됨.
    • 지역 x 소멸 → x strong 2→1(아직 y->next 가 가리킴). 파괴 안 됨.
    • 이제 X·Y 는 서로의 next 로만 1씩 받쳐진다 → strong 0 이 되지 못해 영구 누수.
    • w(weak) 소멸: 영향 없음.
    • 지역 a 소멸 → strong 1→0 → 객체 파괴 → dtor A(마지막 출력).

흔한 오해·함정

  • "shared_ptr 면 누수가 없다"는 오해: 순환 참조는 누수를 만든다. 부모→자식은 shared, 자식→부모(역참조)는 weak 로 끊는다(트리/그래프, 옵저버, 세션↔룸 양방향 등).
  • shared_from_this() 를 생성자에서 호출: 아직 제어블록이 없어 UB/예외. 객체가 shared_ptr 로 소유된 뒤에만 호출.
  • 계수 원자성 ≠ 데이터 안전: 여러 스레드가 같은 객체 내용을 동시 수정하면 별도 동기화 필요. 또 "같은 shared_ptr 인스턴스"를 여러 스레드가 동시에 재대입하는 것도 레이스.
  • 성능: shared_ptr 복사마다 원자 증감. 핫패스는 const shared_ptr&/T*(소유 안 함)로 전달해 불필요한 계수 변동을 피한다. 소유가 단일하면 unique_ptr.

면접 포인트

  1. 제어블록의 strong/weak count 역할과 객체·제어블록 수명 분리. make_shared 의 단일 할당.
  2. 순환 참조 누수의 원인과 weak_ptr 로 끊는 패턴(세션↔룸, 부모↔자식). lock() 의미.
  3. 참조계수는 원자적이지만 객체 내용은 아니다 — 동시성에서 무엇이 보장되고 안 되는지. 핫패스 복사 비용과 unique_ptr/참조 전달.