← 문제로

6. 객체 생성/소멸 순서와 정적 초기화 (static init order fiasco)

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

해설 — 객체 생성/소멸 순서와 정적 초기화 (static init order fiasco)

난이도: 중상

요약

저장기간(storage duration)에 따라 객체의 생성·소멸 시점이 다르다. 전역(정적 저장기간) 은 main 진입 전에 같은 번역 단위 내 정의 순서대로 생성되고, 함수 지역 static 은 그 함수가 처음 실행될 때 딱 한 번 생성된다. 소멸은 모든 정적 객체가 프로그램 종료 시 생성 완료의 역순으로, 자동(automatic) 객체는 스코프를 벗어날 때 역순으로 소멸한다.

출력 예측

ctor g1
ctor g2
enter main
ctor main-local
ctor f-local
ctor localStatic
dtor f-local
ctor f-local        // 두 번째 f(): static 은 다시 만들지 않음
dtor f-local
leave main
dtor main-local
dtor localStatic
dtor g2
dtor g1

핵심 개념

  • 정적 저장기간(전역) 초기화: g1, g2 는 프로그램 시작 시 main 보다 먼저, 한 번역 단위 안에서는 정의된 순서대로 생성된다.
  • 함수 지역 static: static Tracer s(...) 는 제어가 그 선언을 처음 통과할 때 초기화된다(lazy). 이후 호출에서는 다시 초기화하지 않는다 → f() 를 두 번 불러도 ctor localStatic 은 한 번.
  • 자동 저장기간: f-local, main-local 은 블록 진입 시 생성, 블록 종료 시 소멸. f() 가 두 번 불리니 f-local 도 두 번 생성/소멸.
  • 소멸 순서: 정적 객체들은 프로그램 종료 시 생성을 완료한 순서의 역순으로 소멸. 여기서 완료 순서는 g1 → g2 → (main 중) localStatic 이므로 소멸은 localStatic → g2 → g1.

단계별 내부 동작

  1. main 전: ctor g1, ctor g2.
  2. main 진입: enter main, 그다음 ctor main-local.
  3. f(): ctor f-locallocalStatic() 최초 호출이라 ctor localStatic → 함수 끝에서 dtor f-local.
  4. 둘째 f(): ctor f-locallocalStatic() 는 이미 초기화됨(출력 없음) → dtor f-local.
  5. leave main 출력 후 main 반환 → 자동 객체 dtor main-local.
  6. 프로그램 종료 정적 소멸: dtor localStaticdtor g2dtor g1.

흔한 오해·함정

  • static init order fiasco: 서로 다른 번역 단위(.cpp)에 있는 전역 객체들의 초기화 순서는 표준이 정하지 않는다(unspecified). A.cpp 의 전역이 B.cpp 의 전역에 의존하면, B 가 아직 초기화 전일 수 있어 미초기화 객체 사용(UB)이 난다.
  • 해결: Construct-On-First-Use(함수 지역 static). 전역을 직접 두지 말고 T& instance() { static T t; return t; } 로 감싸면, 최초 사용 시점에 초기화가 보장돼 순서 의존이 사라진다(이 코드의 localStatic 형태). 이른바 Meyers 싱글톤.
  • C++11 이후 함수 지역 static 은 초기화가 스레드 안전하다(여러 스레드가 동시에 처음 진입해도 정확히 한 번만, 나머지는 대기). 단 "초기화"가 안전한 것이지, 이후 그 객체에 대한 동시 접근까지 안전한 것은 아니다.
  • 소멸 순서는 "정의 역순"이 아니라 "생성 완료 역순" 이다. lazy 한 지역 static 은 런타임에 생성되므로, 전역보다 늦게 생성돼 전역보다 먼저 소멸할 수 있다(본 예시). 종료 시점에 다른 정적 객체를 참조하는 소멸자는 이미 소멸된 객체를 만질 위험이 있다(static destruction order 문제).

면접 포인트

  1. 저장기간별 생성/소멸 시점(전역=시작 전·정의순, 지역 static=최초 사용 시 1회, 자동=스코프)을 정확히 구분.
  2. static init order fiasco 와 Construct-On-First-Use 해법, C++11 지역 static 의 스레드 안전 초기화.
  3. 소멸은 생성 완료의 역순 — 종료 시 다른 싱글톤을 참조하는 소멸자의 위험(순서 보장 안 됨)을 안다.