Question

Purpose of Unions in C and C++: Active Members and Safe Usage

ccppunionstype-punning

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 ARGB example valid because colour and components are 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.

Take Quiz

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:

  1. Cell contains a kind field and a union named value.
  2. The initializer sets cell.kind to KIND_NUMBER.
  3. It stores 27 in cell.value.number.
  4. The active, meaningful member is now number.
  5. Code can safely use cell.value.number because 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.

Take Quiz

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

NeedC approachC++ approachWhy
Store one of several simple valuesTagged unionstd::variantRecords which alternative is active.
Store all fields simultaneouslystructstructMembers have independent storage.
Convert an object representation to another same-size typememcpystd::bit_cast or memcpyAvoids inactive-union-member access.
Extract fields from a packed integerBit shifts and masksBit shifts and masksIndependent of memory byte order.
Read or write raw object bytesunsigned char access / memcpyunsigned char, std::byte, or memcpyIntended 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.

Take Quiz

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 enum discriminator.
  • In C++, prefer std::variant for 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_cast for representation conversion in C++20+.
  • Use memcpy for 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more