Unions and Anonymous Unions
C++ unions — memory layout, active member rules, type punning with bit_cast, anonymous unions, tagged unions, and std::variant.
unionsince C++98A union is a special class type where all non-static data members share the same memory address, making the object exactly as large as its largest member; at any moment exactly one member is active and reading from any other member is undefined behaviour.
Overview
Unions predate C++ and remain essential in three narrow contexts: low-level bit manipulation, C API interoperability, and space-critical embedded layouts. Outside those contexts, std::variant (C++17) provides the same discriminated-union semantics with type safety and automatic lifetime management.
Understanding unions requires understanding two rules the standard enforces strictly:
- The active member rule. After you write to member
m,mis active. Reading any other member (except through a common initial sequence — see below) is undefined behaviour. - Lifetime of non-trivial members. If a union member has a non-trivial constructor or destructor, you are responsible for calling placement new and the explicit destructor. The compiler will not do it for you.
Syntax
union Name {
T1 member1;
T2 member2;
// ...
};Key rules from the standard:
- All members begin at offset 0.
sizeof(Name)equals the size of the largest member (plus any padding needed for alignment).- A union may have member functions, including constructors and destructors, but no virtual functions and no base classes.
- Prior to C++11, members were restricted to trivially copyable types. C++11 lifted that restriction — non-trivial members are allowed if the union's special members are user-defined.
Examples
Basic layout and active member
union Scalar {
int32_t i;
float f;
double d;
uint8_t bytes[8];
};
static_assert(sizeof(Scalar) == sizeof(double)); // 8 bytes
static_assert(alignof(Scalar) == alignof(double));
Scalar s;
s.i = 0x3F80'0000;
// s.f is now inactive — reading it here is UB
s.f = 1.0f; // activates f, deactivates i
assert(s.f == 1.0f); // OK
// assert(s.i == ...); // UB: i is inactiveType punning — the only safe way (C++20)
Union-based type punning (pun.f = x; return pun.u;) is undefined behaviour in C++ despite being defined in C and widely supported as a GCC/Clang extension. The portable and standard-compliant replacement since C++20 is std::bit_cast:
#include <bit> // C++20
#include <cstdint>
float f = 1.0f;
uint32_t bits = std::bit_cast<uint32_t>(f); // C++20 — no UB
// bits == 0x3F800000
// Reconstruct:
float back = std::bit_cast<float>(bits); // C++20
assert(back == f);std::bit_cast requires both types to have the same size and be trivially copyable. It is constexpr and produces no runtime overhead on any mainstream compiler.
Common initial sequence — the one legal exception
The standard permits reading a non-active member when both the active and inactive members share a common initial sequence of layout-compatible types:
struct Point2D { int x, y; };
struct Point3D { int x, y, z; };
union PointUnion {
Point2D p2;
Point3D p3;
};
PointUnion u;
u.p3 = {1, 2, 3};
// Legal: reading p2.x and p2.y because they form the common initial
// sequence of Point2D and Point3D (both start with int x, int y).
assert(u.p2.x == 1); // OK per [class.union] ¶6
assert(u.p2.y == 2); // OK
// u.p2.z would not exist, and u.p3.z is fine since p3 is activeThis is the mechanism behind many C socket API structs (sockaddr / sockaddr_in / sockaddr_in6).
Anonymous union
An anonymous union has no tag name and no object name; its members are injected directly into the enclosing scope:
struct Packet {
uint8_t type;
union { // anonymous — members accessed as direct fields
uint16_t u16;
uint32_t u32;
float f32;
struct { uint8_t lo, hi; }; // nested anonymous struct (GCC/Clang extension)
};
};
Packet p;
p.type = 2;
p.u32 = 0xDEAD'BEEF;
// p.u16 is now inactive — only p.u32 is valid to readAnonymous unions at namespace scope must be declared static (or have internal linkage). They are commonly used in structs to save space without extra member access syntax.
Non-trivial members — manual lifetime (C++11)
C++11 removed the restriction on non-trivial union members but places the burden of construction and destruction entirely on you:
#include <string>
#include <vector>
#include <new> // placement new
union Storage {
std::string s;
std::vector<int> v;
Storage() {} // trivial no-op — caller must activate a member
~Storage() {} // trivial no-op — caller must destroy active member
};
Storage st;
new (&st.s) std::string("hello"); // activate s
st.s += " world";
st.s.~basic_string(); // destroy before switching
new (&st.v) std::vector<int>{1, 2, 3}; // activate v
st.v.push_back(4);
st.v.~vector(); // must destroy before Storage goes out of scopeForgetting the explicit destructor call is a resource leak. Switching members without destroying the previous one corrupts memory. This is why std::variant exists.
Tagged union pattern
The classical discriminated union — manual, but useful in embedded or performance-critical contexts:
struct Value {
enum class Kind : uint8_t { Null, Bool, Int, Double } kind = Kind::Null;
union {
bool b;
int i;
double d;
};
static Value from_int(int v) { Value r; r.kind = Kind::Int; r.i = v; return r; }
static Value from_double(double v) { Value r; r.kind = Kind::Double; r.d = v; return r; }
double to_double() const {
switch (kind) {
case Kind::Int: return static_cast<double>(i);
case Kind::Double: return d;
case Kind::Bool: return b ? 1.0 : 0.0;
default: return 0.0;
}
}
};With trivially copyable members only, this pattern requires no placement new or explicit destructors and is constexpr-friendly since C++20.
std::variant — the type-safe discriminated union (C++17)
#include <variant> // C++17
using Value = std::variant<std::monostate, bool, int, double, std::string>;
Value v = 3.14;
// Checked access — throws std::bad_variant_access if wrong type
double d = std::get<double>(v);
// Non-throwing pointer access
if (auto* p = std::get_if<double>(&v)) {
// *p is 3.14
}
// Exhaustive dispatch — all alternatives must be handled
std::visit([](auto&& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, std::monostate>) { /* null */ }
else if constexpr (std::is_same_v<T, bool>) { /* bool */ }
else if constexpr (std::is_same_v<T, int>) { /* int */ }
else if constexpr (std::is_same_v<T, double>) { /* double */ }
else { /* string */ }
}, v);
// Overloaded visitor (C++17 deduction guides)
auto printer = [](auto&& x) { std::println("{}", x); }; // std::println: C++23
std::visit(printer, v);std::monostate is a unit type that enables default-constructing a variant whose first alternative is not default-constructible.
std::variant can enter a valueless_by_exception state if an exception is thrown during an in-place assignment. Check with .valueless_by_exception() before accessing. The standard does not provide a strong exception guarantee for operator= when the types differ.
Best Practices
- Default to
std::variantfor any discriminated union involving non-trivial types. The manual approach is too error-prone and adds no performance in most cases. - Use
std::bit_castfor type punning (C++20). If targeting C++17 or earlier, usememcpyinto a local variable of the target type — compilers optimise it to zero instructions on known sizes. - Wrap raw unions in a class that encodes the active-member invariant and manages lifetimes, so callers can never see an inconsistent state.
- Keep union members trivially copyable unless you are deliberately managing lifetimes. The moment you include a
std::stringorstd::vector, the complexity of the manual approach roughly equals writingstd::variantfrom scratch. - Annotate the active member with a comment or accompanying tag whenever the union is a struct field. Silent mistakes happen when maintainers assume the wrong member is active.
Common Pitfalls
Reading an inactive member. This is the most common union bug. It is UB even when the size and alignment of the members are identical. Compilers are permitted to, and do, exploit this for optimisation.
Forgetting to destroy the active member. If the active member has a non-trivial destructor and the enclosing union's destructor does nothing, you leak resources. The compiler issues no warning.
Copying a union with a non-trivial active member. A union's implicitly deleted copy/move constructors must be manually defined if any member is non-trivial. Bitwise copying a std::string union member results in a double-free.
Alignment traps. Union members impose their own alignment requirements. A union { char c; double d; } has alignof(double) alignment — placing it in a __attribute__((packed)) struct can break it.
Assuming std::variant is a union. variant stores an index and the active value, but it does not overlap storage the same way a raw union does. sizeof(std::variant<int, char>) is not sizeof(int) — it includes the index and satisfies alignment for all alternatives.
See Also
std::variant— type-safe discriminated union, C++17std::bit_cast— safe type punning, C++20std::optional— nullable value without a union tag, C++17std::any— type-erased value storage, C++17std::is_union<T>— type trait, C++11- Anonymous structs inside unions (GCC/Clang extension, not standard C++)