union
A union (untagged union) is a composite type in which all fields share the same starting memory offset (0), and the total size of the union is determined by its largest field (padded to the maximum alignment of its fields). Unlike a Rust enum (tagged union), a union stores no discriminant tag to track which field is currently active.
#[repr(C)]
union FloatOrBits {
f: f32, // offset 0..4 (4 bytes, align 4)
bits: u32, // offset 0..4 (4 bytes, align 4)
bytes: [u8; 4], // offset 0..4 (4 bytes, align 1)
}
Overlapping Memory Layout (4 bytes total, align 4):
+-----------+-----------+-----------+-----------+
| f: f32 |
| bits: u32 |
| bytes[0]| bytes[1] | bytes[2] | bytes[3] |
+-----------+-----------+-----------+-----------+
+0 +1 +2 +3
Safe Writing vs. Unsafe Reading Rules
- Creating or writing to a union field is safe: Initializing
FloatOrBits { f: 1.0 }or assigningu.bits = 0x3F80_0000;overwrites the shared storage and does not requireunsafe. - Reading or borrowing a union field requires
unsafe: Because the compiler cannot verify whether the bits currently stored in the union are valid for the field being read (for example, reading aboolor&Tfield when arbitrary bytes were written), every field read (u.bits) or pattern match on a union requires anunsafeblock.
#[repr(C)]
union IpAddressUnion {
octets: [u8; 4],
ipv4_be: u32,
}
fn main() {
// Initializing and overwriting a union field is safe:
let mut addr = IpAddressUnion {
octets: [127, 0, 0, 1],
};
// Reading a union field requires `unsafe`:
// SAFETY: Every 4-byte pattern initialized by `[u8; 4]` is a valid `u32`.
let raw_u32 = unsafe { u32::from_be(addr.ipv4_be) };
println!("127.0.0.1 as big-endian u32: {:#010x}", raw_u32);
addr.ipv4_be = 0xC0A8_0101_u32.to_be();
// SAFETY: Every `u32` bit pattern is a valid `[u8; 4]`.
let octets = unsafe { addr.octets };
println!("0xC0A80101 as octets: {:?}", octets);
}
127.0.0.1 as big-endian u32: 0x7f000001
0xC0A80101 as octets: [192, 168, 1, 1]
Field Type Restrictions and ManuallyDrop<T> in Unions
Because a union does not know which field is active when the union itself goes out of scope, a union cannot automatically drop non-trivial fields. Consequently, a union field must be one of the following:
- A
Copytype (such as primitives, arrays ofCopytypes, raw pointers, or shared references). - A
ManuallyDrop<T>wrapper (which suppresses automaticDropso you can manually drop the active field inside the union's ownimpl Drop).
Attempting to place a non-Copy type with a destructor (like String or Vec<u8>) directly into a union without ManuallyDrop<T> produces compiler error E0740:
// ❌ does not compile
union BadUnion {
num: u64,
text: String,
}
fn main() {}
error[E0740]: field must implement `Copy` or be wrapped in `ManuallyDrop<...>` to be used in a union
--> src/main.rs:4:5
|
4 | text: String,
| ^^^^^^^^^^^^
|
= note: union fields must not have drop side-effects, which is currently enforced via either `Copy` or `ManuallyDrop<...>`
help: wrap the field type in `ManuallyDrop<...>`
|
4 | text: std::mem::ManuallyDrop<String>,
| +++++++++++++++++++++++ +
When to Use union vs. enum
Use union only when interoperating with C libraries (#[repr(C)] union) or implementing low-level space-critical layouts where an external tag tracks the active field. For all general Rust code, use enum (or f32::to_bits / u32::to_ne_bytes for primitive bit conversions).