Question
Consider this union that stores either a 32-bit color value or its individual byte components:
union ARGB {
uint32_t colour;
struct Components {
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
} components;
};
ARGB pixel;
pixel.colour = 0xff040201;
if (pixel.components.a) {
// ...
}
After assigning to pixel.colour, is it valid to read pixel.components.a even though components was not the most recently written union member? If not, what is the intended purpose of unions in C and C++, and what are safe ways to represent values that may have different forms or types?
Short Answer
A union lets several members share the same storage, so an object can hold one of several possible representations without allocating space for all of them. The member most recently stored is generally the active member. C and C++ differ significantly when code reads an inactive member, so portable programs should use unions as tagged "one-of" values rather than as a general byte-conversion technique.
Concept
A union is like a struct whose members occupy the same memory location instead of receiving separate storage.
union Value {
int integer;
float decimal;
};
Value has enough storage for its largest member, not enough storage for both members. Writing integer overwrites the bytes that would otherwise represent decimal.
The primary purpose of a union is to model a value that can be one of several alternatives:
- a parsed token can be an integer, a floating-point number, or an identifier;
- an event can be a mouse event, keyboard event, or window event;
- a protocol message can have different payload layouts based on its type field.
Because the bytes are shared, a union alone does not remember which alternative is currently meaningful. The program must track that information, commonly with a separate tag or discriminator.
C versus C++ when reading another member
The rules are not identical.
- C: Reading a different union member is specified as reinterpreting the stored object representation as the new member type. The resulting value can be unusable if that bit pattern is a trap representation for the type being read. It is also often dependent on the target machine's representation, byte order, padding, and type sizes. Therefore, it is not a good portable technique for decoding bytes.
- C++: In general, reading an inactive union member is undefined behavior. There is a narrow exception for standard-layout struct members with a shared common initial sequence: that shared initial portion may be inspected. This exception does not make the
ARGBexample valid becausecolourandcomponentsare not structs sharing a common initial sequence.
Even in C, the ARGB result is not reliably portable. It assumes that uint32_t has exactly four 8-bit bytes, that there is no relevant padding, and that the machine's byte order places 0xff in the byte expected as a. C++ also adds the inactive-member problem.
For byte-level conversion in modern C++, use std::bit_cast when the types meet its requirements. For copying bytes in either language, use memcpy.
Mental Model
Think of a union as one parking space with several possible signs above it: Car, Motorcycle, and Bicycle. Only one vehicle is actually parked there at a time.
The signs describe possible interpretations of the same space; they do not create separate parking spaces. If you park a car and then treat the same space as if it contained a bicycle, you are guessing from the occupied bytes rather than accessing a separately stored bicycle.
A tag is the parking ticket that says what is currently parked:
enum class Kind { Integer, Decimal };
The tag tells the program which union member it is allowed to read.
Syntax and Examples
Basic union syntax
C
union Value {
int integer;
double decimal;
};
union Value value;
value.integer = 42;
printf("%d\n", value.integer);
C++
union Value {
int integer;
double decimal;
};
Value value;
value.integer = 42;
std::cout << value.integer << '\n';
In both examples, integer is the member written most recently, so reading integer is the intended operation.
A safer tagged union in C
#include <stdio.h>
enum ValueKind {
VALUE_INTEGER,
VALUE_DECIMAL
};
struct Value {
enum ValueKind kind;
union {
int integer;
double decimal;
} data;
};
void print_value(struct Value value) {
if (value.kind == VALUE_INTEGER) {
printf("integer: %d\n", value.data.integer);
} else {
printf("decimal: %.2f\n", value.data.decimal);
}
}
int main(void) {
struct Value value = { VALUE_DECIMAL, { .decimal = 3.5 } };
print_value(value);
}
The kind field records which member is active. print_value uses that field before selecting a member to read.
Preferred C++ alternative: std::variant
#include <iostream>
#include <variant>
int main() {
std::variant<int, double> value = 3.5;
if (const auto* decimal = std::get_if<double>(&value)) {
std::cout << *decimal << '\n';
}
}
std::variant is a tagged union provided by the C++ standard library. It tracks the active alternative and prevents accidental reads of the wrong one.
Step by Step Execution
Consider this C-style tagged union:
enum Kind { KIND_NUMBER, KIND_TEXT };
struct Cell {
enum Kind kind;
union {
int number;
const char *text;
} value;
};
struct Cell cell = { KIND_NUMBER, { .number = 27 } };
Execution proceeds as follows:
Cellcontains akindfield and a union namedvalue.- The initializer sets
cell.kindtoKIND_NUMBER. - It stores
27incell.value.number. - The active, meaningful member is now
number. - Code can safely use
cell.value.numberbecause the tag says the cell contains a number.
if (cell.kind == KIND_NUMBER) {
printf("%d\n", cell.value.number);
}
If code instead ran puts(cell.value.text);, it would interpret the bytes of the integer 27 as a pointer. That does not create a valid string pointer and can crash or otherwise misbehave.
For the original color example, assigning this value:
pixel.colour = 0xff040201;
stores one integer value. It does not portably establish four separately stored component values. On a little-endian machine, the bytes in increasing address order will commonly be 01 02 04 ff; on a big-endian machine they will commonly be ff 04 02 01. This is why union-based color component access is dependent on platform representation even before considering C++ inactive-member rules.
Real World Use Cases
Unions are useful when exactly one alternative is needed at a time and memory layout matters.
- Compilers and interpreters: A token may contain an integer literal, a floating-point literal, or an identifier reference.
- Event systems: An event type determines whether its payload is mouse coordinates, a key code, or resize dimensions.
- Network and binary protocol parsers: A message header identifies which payload structure follows.
- Embedded systems: A hardware driver may work with several command payload formats while avoiding unnecessary memory use.
- Operating system and C APIs: Many APIs expose tagged unions for optional fields or message-specific data.
A color value is usually better represented directly as components:
struct Color {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
Convert to or from a packed integer explicitly when a file format, graphics API, or network protocol requires it.
Real Codebase Usage
In real C projects, unions are commonly placed inside a struct with a discriminator:
enum MessageType {
MESSAGE_LOGIN,
MESSAGE_LOGOUT
};
struct Message {
enum MessageType type;
union {
struct {
const char *username;
} login;
struct {
int reason_code;
} logout;
} payload;
};
Code checks the discriminator before accessing the payload:
if (message.type != MESSAGE_LOGIN) {
return;
}
printf("User: %s\n", message.payload.login.username);
This guard-clause style makes the active-member rule visible and prevents using the wrong payload.
In C++ codebases, prefer std::variant for ordinary application data because it stores the active alternative and supplies checked access through std::get, std::get_if, and std::visit.
For packed values, use explicit bit operations rather than relying on union layout:
#include <cstdint>
std::uint32_t pack_argb(std::uint8_t a, std::uint8_t r,
std::uint8_t g, std::uint8_t b) {
return (static_cast<std::uint32_t>(a) << 24) |
(static_cast<std::uint32_t>(r) << 16) |
(static_cast<std::uint32_t>(g) << 8) |
static_cast<std::uint32_t>(b);
}
std::uint8_t alpha_from_argb(std::uint32_t colour) {
return static_cast<std::uint8_t>(colour >> 24);
}
This defines the desired numeric format independently of CPU byte order.
Common Mistakes
Reading a different member as a type conversion
union Number {
float floating;
std::uint32_t bits;
};
Number number;
number.floating = 1.0f;
std::uint32_t raw = number.bits; // Not portable C++; inactive-member access
Do not use a union as a general C++ type-punning conversion. Use std::bit_cast in C++20 or later:
#include <bit>
#include <cstdint>
std::uint32_t raw = std::bit_cast<std::uint32_t>(1.0f);
For older C++ or for C, use memcpy when appropriate:
#include <stdint.h>
#include <string.h>
float source = 1.0f;
uint32_t raw;
memcpy(&raw, &source, sizeof raw);
Forgetting to track the active member
union Data {
int id;
const char *name;
};
This type does not reveal whether id or name currently holds a meaningful value. Put it in a struct with an enum tag.
Assuming a packed integer has a universal byte order
pixel.colour = 0xff040201;
The numeric shifts in this literal are clear, but the order of its bytes in memory varies by endianness. Do not assume a union of an integer and four bytes gives a portable ARGB layout.
Using char when an exact unsigned byte is required
Plain char may be signed or unsigned depending on the implementation. Use uint8_t when it exists and the format requires 8-bit unsigned units. For raw object bytes, unsigned char is the traditional C and C++ byte-oriented type.
Putting non-trivial C++ objects in a union without managing lifetime
C++ unions that contain types such as std::string require explicit construction and destruction of the active member. Prefer std::variant unless low-level manual lifetime management is necessary.
Comparisons
| Need | C approach | C++ approach | Why |
|---|---|---|---|
| Store one of several simple values | Tagged union | std::variant | Records which alternative is active. |
| Store all fields simultaneously | struct | struct | Members have independent storage. |
| Convert an object representation to another same-size type | memcpy | std::bit_cast or memcpy | Avoids inactive-union-member access. |
| Extract fields from a packed integer | Bit shifts and masks | Bit shifts and masks | Independent of memory byte order. |
| Read or write raw object bytes | unsigned char access / memcpy | unsigned char, std::byte, or memcpy | Intended for byte-level operations. |
Union versus struct
struct Both {
int id;
float score;
};
A struct stores both id and score at the same time.
union Either {
int id;
float score;
};
A union has storage for one alternative at a time. It is appropriate only when the alternatives are mutually exclusive.
Union versus inheritance in C++
Inheritance models an is-a relationship and supports polymorphic behavior. A union or std::variant models a one-of-these-alternatives value. For a closed list of value alternatives, std::variant is often clearer than a class hierarchy.
Cheat Sheet
- A union's members share the same storage.
- The union size is generally at least large enough for its largest member, subject to alignment and implementation rules.
- After writing one member, read that same member unless a language rule explicitly permits otherwise.
- A union does not automatically track the active member.
- In C, pair a union with an
enumdiscriminator. - In C++, prefer
std::variantfor most tagged-union use cases. - In C++, reading an inactive member is generally undefined behavior.
- C++ permits inspecting a shared common initial sequence of certain standard-layout struct union members; this is a narrow exception.
- Do not use an integer-and-byte union as portable color-channel extraction.
- Use shifts and masks for packed numeric formats.
- Use
std::bit_castfor representation conversion in C++20+. - Use
memcpyfor portable byte copying in C and in earlier C++. - Byte order, padding, and invalid representations can make representation-based code non-portable.
FAQ
What is the main purpose of a union in C and C++?
A union represents a value that may be one of several alternatives while reusing the same memory for each alternative. It is commonly used with a tag that identifies the active alternative.
Can I read a union member that was not written most recently in C++?
Generally, no. It is undefined behavior, apart from limited cases such as inspecting a common initial sequence shared by certain standard-layout struct members.
Is union type punning valid in C?
C provides rules for reinterpreting a union's stored object representation through another member, but the value can be invalid for the target type and the result can depend on machine representation. It is not a portable replacement for explicit conversion or byte copying.
Why does the ARGB union produce different results on different machines?
The memory order of the bytes in a multi-byte integer depends on endianness. A numeric constant such as 0xff040201 has the same numeric value, but its byte layout in memory can differ.
How do I safely get the alpha byte from a packed ARGB integer?
Define the packed format numerically and use a shift:
std::uint8_t alpha = static_cast<std::uint8_t>(colour >> 24);
This assumes your format defines alpha in bits 24 through 31, not in a particular memory byte position.
Should I use a union or std::variant in modern C++?
Use std::variant for most application code. Use a raw union when you need a specific low-level layout and are prepared to manage active members and object lifetimes correctly.
Does a union save memory?
It can. A union allocates shared storage sized for its largest member rather than storage for every member. A tag added to make it safe also requires space, so measure when memory is important.
Mini Project
Description
Build a small color utility that represents a color as a packed ARGB integer while extracting and modifying channels with bit operations. This demonstrates the portable alternative to reading a different union member for color components.
Goal
Create functions that pack ARGB components, unpack them, and change only the alpha channel without depending on union layout or CPU byte order.
Requirements
Create a ColorComponents struct containing a, r, g, and b fields.
Write a function that packs four 8-bit components into one uint32_t ARGB value.
Write a function that unpacks a uint32_t ARGB value into ColorComponents.
Write a function that replaces only the alpha component.
Demonstrate the functions with the packed value 0xff040201.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.