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(멤버 크기) + 인덱스 + 정렬이다(공용체이므로 합이 아니라 최댓값 기준).
단계별 내부 동작
- 저장: variant는 모든 대안을 담을 수 있는 크기·정렬의 raw 저장소를 갖고, 그 안에 현재 대안을 제자리(placement new) 로 생성한다. 동시에 작은 정수 인덱스로 "지금 몇 번 대안인지"를 기록한다.
- 대입/교체: 새 값을 대입하면 기존 대안의 소멸자를 호출하고 새 대안을 그 자리에 생성한 뒤 인덱스를 갱신한다.
- std::visit: 컴파일러가 "대안 개수만큼"의 호출 지점(각 대안에 대해 방문자를 호출하는 함수)을 만들어 함수 포인터 배열(점프 테이블) 을 구성한다. 런타임엔
table[m.index()](visitor, storage)한 번의 간접 호출로 분기한다. if/else 사슬이 아니라 O(1) 테이블 색인이다. - 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로 모델링하면 동적 할당 없이 타입 안전 디스패치가 가능. 단 가장 큰 메시지가 전체 크기를 좌우하니 핫패스 메시지 크기에 주의.