C++ union은 왜 위험한가? active member부터 std::variant까지
한 줄 답: union은 여러 멤버가 하나의 메모리 공간을 공유해 크기를 줄이는 구조이지만, 지금 어느 멤버가 유효한지(active member)를 프로그래머가 직접 추적해야 하는 위험이 따릅니다. C++17부터는 이를 타입 안전하게 관리하는 std::variant를 대안으로 사용합니다.
이 글은 A Tour of C++ 2.5 Unions 학습노트를 기준으로 정리한 내용입니다. 2.3 Classes의 상세 설명이나 concepts, ranges는 다루지 않고, std::variant도 tagged union의 대안이라는 범위 안에서만 살펴봅니다.
union 멤버는 왜 같은 메모리를 쓰나요?
한 줄 답: struct와 달리 union의 모든 멤버가 같은 저장 공간을 함께 씁니다.
struct에서는 멤버마다 각자의 저장 공간이 따로 있습니다. 반면 union에서는 모든 멤버가 하나의 저장 공간을 공유합니다. 다음은 학습노트에 있는 예제입니다.
union Data {
int i;
double d;
char c;
};
int가 4바이트, double이 8바이트, char가 1바이트인 일반적인 환경이라면 sizeof(Data)는 보통 8바이트가 됩니다. union 전체 크기는 가장 큰 멤버를 기준으로 정해지며, 정렬(alignment) 등 구현 세부사항의 영향을 받을 수 있습니다.
이런 특징 때문에 union은 메모리를 극단적으로 절약해야 하는 상황에서 쓰입니다.

struct와 union 메모리 레이아웃 비교
active member를 잘못 읽으면 어떻게 되나요?
한 줄 답: union에서는 마지막으로 값을 저장한 멤버만 유효(active)하고, 이전 멤버의 값은 별도 공간에 남아 있지 않습니다.
union의 멤버들은 독립된 공간이 아니라 같은 공간을 공유하므로, 한 시점에는 하나의 멤버 값만 유효한 값으로 취급해야 합니다. 학습노트의 예제로 살펴보면 다음과 같습니다.
data.i = 100; // active: i
data.d = 5.5; // active: d
data.i = 100;을 실행한 뒤 data.d = 5.5;를 실행하면, 이제 active member는 d가 됩니다. i에 저장했던 100이라는 값은 별도 공간에 보존되지 않습니다.
학습 문제로 등장한 다음 순서도 같은 원리를 보여줍니다.
v.i = 10;
v.d = 3.14;
v.i = 20; // active: i, 값 20
v.i = 10; 다음 v.d = 3.14;를 실행하면 active member가 d로 바뀌고, 다시 v.i = 20;을 실행하면 active member는 i로 돌아오며 값은 20이 됩니다. 이 상태에서 v.d를 읽으면 3.14가 아니라 i에 저장된 비트 패턴을 double로 잘못 해석한 값이 나오거나, 미정의 동작(undefined behavior)으로 이어질 수 있습니다.

active member가 바뀌는 과정
tagged union 대신 std::variant를 쓰는 이유는?
한 줄 답: tagged union은 태그와 실제 active member의 일치를 컴파일러가 보장하지 않지만, std::variant는 현재 타입을 스스로 관리하고 잘못 접근하면 예외를 던집니다.
raw union은 현재 어떤 멤버가 유효한지 프로그래머가 직접 관리해야 합니다. 이를 보완하기 위해 전통적으로 enum 태그와 union을 함께 묶는 tagged union 방식을 사용했습니다. 학습노트의 예제는 다음과 같습니다.
enum class Type { ptr, num };
struct Entry {
Type type;
union { Node* p; int i; };
};
문제는 컴파일러가 type 필드와 실제로 저장된 union 멤버가 서로 일치하는지 자동으로 보장해 주지 않는다는 점입니다. type을 Type::num으로 설정해 놓고 실수로 p에 값을 저장해도 컴파일러는 이를 막지 않습니다. 결국 태그와 실제 active member의 동기화는 전적으로 프로그래머의 책임입니다.
C++17에서 도입된 std::variant는 이 문제를 타입 시스템 차원에서 해결합니다. 학습노트의 예제를 보면 다음과 같습니다.
std::variant value;
value = 100; // int
value = 3.5; // double
int x = std::get(value); // 현재 타입이 int가 아니면 std::bad_variant_access
std::variant는 현재 어떤 타입이 저장되어 있는지를 스스로 추적하므로 별도의 태그를 직접 동기화할 필요가 없습니다. std::get으로 현재 저장된 타입과 다른 타입을 요청하면 std::bad_variant_access 예외가 발생해, tagged union처럼 조용히 잘못된 값을 읽는 상황을 막아줍니다.
정리하면, union은 메모리를 직접 공유하고 active member를 수동으로 관리해야 하는 저수준·임베디드·프로토콜 코드에서 접하게 되고, std::variant는 타입 자체를 스스로 추적해 더 타입 안전한 대안이 됩니다.

tagged union과 std::variant 비교
FAQ
Q. union은 언제 주로 사용됩니까?
A. 메모리 제약이 심한 저수준 프로그래밍이나 임베디드, 네트워크 프로토콜에서 주로 볼 수 있습니다.
Q. std::variant는 메모리 오버헤드가 없습니까?
A. 내부적으로 현재 상태(타입)를 추적하기 위한 추가 공간이 필요합니다.
출처
C++ 학습노트 — 2.5 Unions