22. 멤버 초기화 리스트의 실행 순서
난이도 중내 리뷰 · C++
해설 · C++
해설 — 멤버 초기화 리스트의 실행 순서
난이도: 중상
답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트
요약
멤버 초기화 리스트에 어떤 순서로 적었는지는 실행 순서와 무관하다. C++ 표준은 멤버가
클래스에 선언된 순서대로 초기화된다고 규정한다. PacketBuffer 는 초기화 리스트에
size_(capacity_ / 2), capacity_(capacity) 라고 "작성"했지만, 실제 클래스 선언 순서는
size_ 가 먼저, capacity_ 가 나중이다. 따라서 실행 순서도 size_ 먼저, capacity_
나중이며, size_ 를 초기화하는 시점에 capacity_ 는 아직 초기화되지 않은 채 읽힌다
(미초기화 값 사용, 사실상 UB — 이번 환경에서는 우연히 0이 나왔을 뿐 값이 보장되지 않는다).
Session 의 멤버들도 선언 순서(id_ → buf_ → name_)대로 초기화되고, 소멸은 그 역순
(name_ 은 트리비얼이라 실질적으로 buf_ → id_ 순으로 자원이 정리되며, ~PacketBuffer
가 ~Session 본문 이후에 자동 호출된다).
출력 예측
--- constructing Session ---
capacity_=64, size_=<불확정값, 이 환경에선 0으로 관측>
Session ctor done: id=1, name=session
--- destructing Session ---
~Session
~PacketBuffer (capacity_=64)
size_ 값은 미초기화 메모리를 읽은 결과라 이식 가능한 보장이 없다(0이 나오는 것은
우연이며 최적화 옵션·플랫폼에 따라 쓰레기값이 나올 수 있다).
핵심 개념
- 초기화 순서의 유일한 기준: 멤버 선언 순서. 초기화 리스트(mem-initializer-list)에 적은 순서는 가독성을 위한 표기일 뿐 실행 순서를 바꾸지 못한다. 컴파일러는 리스트를 선언 순서에 맞게 재정렬해서 실행한다.
- 기반 클래스 → 멤버(선언 순서) → 생성자 본문. 상속이 있다면 기반 클래스 서브오브젝트가 가장 먼저, 그 다음 멤버들이 선언 순서대로, 마지막에 생성자 본문이 실행된다.
- 소멸은 정확히 역순: 생성자 본문 종료 → 멤버(선언 역순) 소멸자 → 기반 클래스 소멸자.
Session의 경우buf_(PacketBuffer)의 소멸자가id_, 기반 클래스보다 늦게 선언됐으므로~Session본문 이후 가장 먼저 소멸된다.
단계별 내부 동작
- 컴파일러는 클래스 정의를 훑어 멤버 선언 순서 테이블을 만든다:
PacketBuffer는[size_, capacity_],Session은[id_, buf_, name_]. - 생성자가 호출되면, 초기화 리스트에 무슨 순서로 적혀 있든 이 선언 순서 테이블 순으로 각 멤버의 초기화 코드를 실행한다.
PacketBuffer(64)호출: 먼저size_(capacity_ / 2)를 평가하려 하는데, 이 시점의capacity_는 아직 자신의 초기화 코드가 실행되기 전이라 값이 정해지지 않은 채 (스택/멤버 슬롯의 임의 바이트) 읽힌다 →size_는 불확정 값이 된다. 그 다음capacity_(capacity)가 실행돼capacity_는 64로 정상 초기화된다.Session(64)호출:id_(1)→buf_(cap)(위 과정 실행) →name_("session")순으로 실행되고 마지막에 생성자 본문이 돈다.- 소멸 시점:
~Session본문 실행 후, 멤버가 선언 역순으로 소멸 —name_(트리비얼, 실제 소멸자 없음) →buf_(~PacketBuffer호출, "capacity_=64" 출력) →id_(트리비얼). 기반 클래스가 있었다면 마지막에 소멸됐을 것이다.
흔한 오해·함정
- "초기화 리스트에 적은 순서대로 실행된다" ✗ — 가장 흔한 오해. 실제 실행 순서는 오직 멤버 선언 순서로 결정된다. 리스트 순서는 컴파일러가 참고하지 않는다(단, 가독성을 위해 선언 순서와 일치시키는 것이 관례이자 안전장치다).
- 컴파일러가 항상 잡아준다? △ — 이 코드는 리스트 작성 순서(
size_, capacity_)와 실제 선언 순서(size_, capacity_)가 우연히 일치하므로-Wreorder(GCC/Clang,-Wall포함 — "리스트 순서가 선언 순서와 다르다"를 잡는 경고)는 뜨지 않는다. 대신size_가 아직 초기화되지 않은capacity_를 읽는다는-Wuninitialized경고만 뜬다(직접g++ -std=c++17 -Wall로 재검증 완료). 즉 이 문제의 함정은 "리스트 순서가 선언 순서와 달라서" 생기는 게 아니라, 순서가 같더라도 멤버 간 초기화 의존성 방향이 선언 순서와 반대이면 여전히 미초기화 값을 읽을 수 있다는 점이다 —-Wreorder는 만능이 아니며, 리스트·선언 순서가 일치해도 의존성 문제는 컴파일러가 자동으로 잡아주지 않는다. - "멤버 순서를 바꾸면 성능/의미가 같다" ✗ — 이 예제처럼 멤버 간 초기화 의존성이
있으면 선언 순서를 바꾸는 것만으로 동작이 완전히 달라진다. 실무 규칙: 초기화 리스트의
작성 순서를 항상 선언 순서와 일치시켜라. 이렇게 하면 적어도 "적은 대로 실행된다"는
직관이 코드를 읽는 사람에게 성립하고(이 예제처럼 리스트·선언 순서가 이미 같아도
-Wreorder는 뜨지 않으므로, 의존성 자체를 없애거나 팩토리/2단계 초기화로 우회하는 것이 근본 해결책이다). - "어차피 값 다 초기화되니 괜찮다" ✗ — 선언 순서상 뒤에 있는 멤버는 앞선 멤버의 초기화 시점에는 존재하되 아직 초기화되지 않은 상태다. 이를 읽는 것은 값이 정의되지 않은 동작이며, 최적화 빌드에서는 완전히 예측 불가능한 값이 나올 수 있다.
면접 포인트 (예상 질문)
- 멤버 초기화 순서를 실제로 결정하는 것은 무엇인가? 초기화 리스트에 적은 순서가 왜 실행 순서를 바꾸지 못하는가?
- 이 코드에서 어떤 컴파일러 경고가 뜨는지, 그 경고가 가리키는 실제 위험은 무엇인가?
- 멤버 간에 초기화 의존성이 있을 때 안전하게 작성하는 방법은? (선언 순서와 리스트 순서 일치, 또는 팩토리 함수/2단계 초기화로 의존성 제거)
- 소멸 순서는 초기화 순서와 어떤 관계인가? 기반 클래스가 있다면?