← 문제로

15. std::variant 와 std::visit 의 내부 동작

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

해설 — std::variant 와 std::visit 의 내부 동작

난이도: 상

요약

std::variant는 "여러 대안 타입 중 하나를 담는 타입 안전 공용체"다. 내부적으로 **정렬된 저장공간(union 류) + 현재 어느 대안인지 가리키는 정수 인덱스(discriminant)**로 구현된다. std::visit는 이 인덱스로 컴파일 타임에 생성된 함수 포인터 테이블(점프 테이블)을 색인해 알맞은 오버로드를 호출한다.

예측 출력(실측)

0 Move:3
1 Chat:hi
2 Attack:42
holds<Chat>=false
sizeof(Msg)=40
  • index(): 현재 담긴 대안의 0-기반 위치. Move→0, Chat→1, Attack→2. 대입할 때마다 인덱스가 갱신된다.
  • holds_alternative<Chat>(m): 마지막 대입이 Attack(index 2)이므로 false.
  • sizeof(Msg)=40: 대안 중 가장 큰 것은 Chat(libstdc++의 std::string = 32B). 거기에 discriminant와 정렬 패딩이 더해져 40B. variant 크기 ≈ max(멤버 크기) + 인덱스 + 정렬이다(공용체이므로 합이 아니라 최댓값 기준).

단계별 내부 동작

  1. 저장: variant는 모든 대안을 담을 수 있는 크기·정렬의 raw 저장소를 갖고, 그 안에 현재 대안을 제자리(placement new) 로 생성한다. 동시에 작은 정수 인덱스로 "지금 몇 번 대안인지"를 기록한다.
  2. 대입/교체: 새 값을 대입하면 기존 대안의 소멸자를 호출하고 새 대안을 그 자리에 생성한 뒤 인덱스를 갱신한다.
  3. std::visit: 컴파일러가 "대안 개수만큼"의 호출 지점(각 대안에 대해 방문자를 호출하는 함수)을 만들어 함수 포인터 배열(점프 테이블) 을 구성한다. 런타임엔 table[m.index()](visitor, storage) 한 번의 간접 호출로 분기한다. if/else 사슬이 아니라 O(1) 테이블 색인이다.
  4. overloaded 트릭: struct overloaded : Ts... 가 여러 람다의 operator()를 한 타입으로 상속(using 선언으로 모두 노출)해, visit이 인자 타입에 맞는 오버로드를 정적으로 고르게 한다. C++17 CTAD(추론 가이드)로 overloaded{...} 생성이 가능.

흔한 오해·함정

  • valueless_by_exception: 대안을 교체하던 중(기존 소멸 후 새 대안 생성 중) 생성자가 예외를 던지면 variant가 "어떤 대안도 담지 않은" 무효 상태가 될 수 있다. 이때 index()==variant_npos, valueless_by_exception()==true이고, 그 상태에 visit하면 std::bad_variant_access가 난다. 이동 생성이 noexcept면 이 상태를 피하도록 구현이 노력한다.
  • variant는 힙을 쓰지 않는다(대안을 인라인 저장). 그래서 크기가 가장 큰 대안에 좌우된다 — 큰 타입 하나가 전체 크기를 키운다. 큰 대안은 unique_ptr로 감싸 크기를 줄이는 기법이 있다.
  • get<T>는 타입이 안 맞으면 예외(bad_variant_access), get_if<T>는 포인터(실패 시 nullptr). 인덱스로도 접근 가능(get<0>).
  • 공용체와 달리 타입 안전하다: 잘못된 대안 접근이 컴파일/런타임에 잡힌다.

면접 포인트

  • "variant는 어떻게 어느 타입인지 아나?" — 저장소 + discriminant 인덱스. visit은 인덱스로 점프 테이블을 색인해 O(1) 분기.
  • "sizeof가 왜 합이 아니라 최댓값?" — 공용체처럼 한 시점에 한 대안만 살아있으므로 가장 큰 대안 크기 + 인덱스 + 정렬.
  • valueless_by_exception이 생기는 조건과 회피책(이동 생성 noexcept 보장, 큰 대안은 포인터로 감싸기)을 설명할 수 있어야 한다.
  • 게임 서버 맥락: 프로토콜 메시지/이벤트를 variant로 모델링하면 동적 할당 없이 타입 안전 디스패치가 가능. 단 가장 큰 메시지가 전체 크기를 좌우하니 핫패스 메시지 크기에 주의.