C++ union은 왜 위험한가? active member부터 std::variant까지

Short answer: A union lets several members share one block of memory to save space, but it puts the burden of tracking which member is currently valid — the active member — on the programmer. Since C++17, std::variant offers a type-safe alternative that manages this for you.

This post follows the A Tour of C++ 2.5 Unions study notes. It stays out of the 2.3 Classes discussion and out of concepts/ranges, and treats std::variant only as an alternative to a hand-rolled tagged union.

Why do union members share the same memory?

Short answer: Unlike a struct, every member of a union shares one storage location instead of getting its own.

In a struct, each member has its own separate storage. In a union, all members share a single storage location. The study notes give this example:

union Data {
    int i;
    double d;
    char c;
};

On a typical platform where int is 4 bytes, double is 8 bytes, and char is 1 byte, sizeof(Data) is usually 8 bytes. The overall size of a union is generally determined by its largest member, though alignment and other implementation details can affect this.

This is exactly why unions show up when memory needs to be conserved as aggressively as possible.

struct vs union memory layout

struct vs union memory layout

What happens if you read the wrong active member?

Short answer: Only the member most recently written to is the active member, and earlier members’ values are not preserved anywhere else.

Because union members do not have independent storage, only one member’s value should be treated as valid at any given moment. The study notes illustrate this as follows:

data.i = 100; // active: i
data.d = 5.5; // active: d

After data.i = 100;, running data.d = 5.5; makes d the active member. The 100 that was stored in i is not kept anywhere separately.

A follow-up example from the same notes makes the same point with an extra step:

v.i = 10;
v.d = 3.14;
v.i = 20; // active: i, value 20

v.i = 10; then v.d = 3.14; switches the active member to d, and v.i = 20; switches it back to i with the value 20. Reading v.d at this point does not give back 3.14 — it reinterprets the bit pattern stored for i as a double, which produces a meaningless value or, more generally, undefined behavior.

how the active member changes

how the active member changes

Why use std::variant instead of a tagged union?

Short answer: A tagged union relies on the programmer to keep its tag in sync with the actual stored member, while std::variant tracks its own type and throws when accessed incorrectly.

A raw union leaves it entirely up to the programmer to remember which member is currently valid. The traditional workaround is the “tagged union” pattern: pairing an enum tag with the union. The study notes give this example:

enum class Type { ptr, num };
struct Entry {
    Type type;
    union { Node* p; int i; };
};

The problem is that the compiler never checks that type actually matches the member that was last written. If type is set to Type::num but the code mistakenly writes to p instead, nothing stops it. Keeping the tag and the real active member in sync is entirely the programmer’s responsibility.

std::variant, introduced in C++17, solves this at the type-system level. The study notes’ example:

std::variant value;
value = 100; // int
value = 3.5; // double
int x = std::get(value); // throws std::bad_variant_access if the current type isn't int

std::variant tracks its own currently held type, so there is no separate tag to keep in sync by hand. Asking std::get for a type that isn’t currently active throws std::bad_variant_access, which turns what would silently be a wrong value in a tagged union into a visible, catchable error.

In short: a union shares memory directly and needs manual active-member tracking, which is why it still shows up in low-level, embedded, and protocol code, while std::variant tracks its own type and is the more type-safe modern alternative.

tagged union vs std::variant

tagged union vs std::variant

FAQ

Q. When are unions typically used?

A. Mostly in low-level programming with tight memory constraints, embedded systems, and network protocols.

Q. Does std::variant have no memory overhead?

A. No — it needs extra internal space to track which type is currently active.

Sources

C++ study notes — 2.5 Unions

cppreference: union

cppreference: std::variant