Concepts / Understanding Unions in C++

Understanding Unions in C++

A union holds a value of one of its member types at a time, but the language does not record which type is held.

  • Programming

Why a Union Cannot Answer Its Own Question

A union is a type that can hold a value of one of several member types. Only one member is in use at any moment. The question that matters in practice is simple: when you look at a union, which member was written last? For a union, the language does not answer that question for you. This article follows the state of one union through a few writes and reads, and shows where the answer has to come from instead.

What do you think happens?

You write an int into a union, then read the same union through its double member. What do you think the program gets back?

Reveal answer
Answer:

The bytes that hold the int are read as a double. The result is not the int that was written, and the language gives no error signal that the type was wrong.

The union stores bits, and the program decides how to interpret them. Nothing in the union records that the last write used the int member, so the read simply reinterprets the bytes.

What a Union Stores

Definition

A union is a type whose members share one region of storage. At any moment it holds a value of only one of its member types. When you write a new value into the union, the previous value is replaced. The union itself does not say which member was written last.

Because the members share storage, the union is best pictured as one block of bytes with several names looking at it. The diagram below shows two member names, one read as an int and one read as a double, both pointing at the same block. Writing through one name changes the bytes that the other name reads.

int member ireads the bytes as an intShared storageholds the bytes of the lastwritedouble member dreads the same bytes as adoubleviewview
If I write through the int member and then read through the double member, which bytes does each read touch?

The consequence is that a read through a member does not check whether that member was the one written. It simply interprets whatever bits are there. The language does not flag the mismatch, which is why the programmer has to keep track of it.

Following the Active Member Step by Step

Trace a union through three states: empty, holding an int, and then holding a double. Each assignment replaces the previous value. The active type changes at each step, and so should any record the program keeps about it.

Emptyno member written yetInt 42active member: iDouble 3.5active member: dwrite int 42write double 3.5
After writing an int and then a double, what is the active type at each step, and what should the record say?

Notice that the language does not produce this record. After each write, the union holds only the new value, and the previous active member is no longer represented anywhere inside the union. The record of which member is active has to be kept by the program.

Who Keeps the Record

The programmer is responsible for tracking the type of value a union currently holds. A common way to meet this responsibility is to store a second piece of state next to the union: a small value that records which member is active. This pairing is often called a tagged union. The union holds the data, and the tag says how to read it.

TaggedValuewrapper structkindrecords the active membermember iintValue unionholds the datamember ddoublecontainscontainsmembermember
In a struct that holds a type tag and a union, which part remembers the active member, and which part holds the value?

A Tagged Union in Code

The program below is an original illustration of the tagged pattern. The union Value holds either an int or a double. The struct TaggedValue stores the union together with an enum that records which member was written last. Each write function updates the tag, and the print function reads the tag before choosing a member.

Code walkthrough
#include <iostream>

union Value {
    int i;
    double d;
};

enum class Kind { None, Int, Double };

struct TaggedValue {
    Kind kind = Kind::None;
    Value value;
};

void write_int(TaggedValue& tv, int x) {
    tv.value.i = x;
    tv.kind = Kind::Int;
}

void write_double(TaggedValue& tv, double x) {
    tv.value.d = x;
    tv.kind = Kind::Double;
}

void print(const TaggedValue& tv) {
    switch (tv.kind) {
        case Kind::Int:    std::cout << tv.value.i << std::endl; break;
        case Kind::Double: std::cout << tv.value.d << std::endl; break;
        case Kind::None:   std::cout << "empty" << std::endl; break;
    }
}

int main() {
    TaggedValue tv;
    print(tv);
    write_int(tv, 42);
    print(tv);
    write_double(tv, 3.5);
    print(tv);
}
  1. 1
    The struct TaggedValue pairs the tag kind with the union value.
  2. 2
    write_int stores 42 in member i and sets kind to Int.
  3. 3
    print sees Kind::Int and reads member i, printing 42.
  4. 4
    write_double replaces the stored value with 3.5 in member d and sets kind to Double.
  5. 5
    print sees Kind::Double and reads member d, printing 3.5.
  • The union Value stores one member at a time
  • The enum Kind is the tag that records the active member
  • Each write function updates both the union and the tag
  • print consults the tag before reading a member
Output
empty
42
3.5

Reading the Same Bytes Two Ways

The next comparison shows what goes wrong when the record is ignored. Suppose the program writes an int and then reads the union through its double member. The bytes do not change during the read. Only their interpretation changes, so the value that comes back is not the int that was written. The guarded version checks the tag first and reads only the member that matches it.

Before
TaggedValue tv;
write_int(tv, 42);
// no check: reads member d, which was never written
std::cout << tv.value.d << std::endl;

The tag says Int, but the read ignores the tag and interprets the bytes as a double.

After
TaggedValue tv;
write_int(tv, 42);
// check the tag, then read the member it names
if (tv.kind == Kind::Int) {
    std::cout << tv.value.i << std::endl;
}

The read consults the tag and interprets the bytes as the int that was written.

The language gives no clear signal in the first version. The program prints a value that looks like a number but is not the one the programmer intended. The guarded version avoids this by letting the tag decide which interpretation applies.

Mistakes When the Type Is Not Tracked

  • Common mistake

    Assuming the union remembers which member was written last

    What it looks like

    Writing an int and then reading the double member, expecting the int value back.

    Why it fails

    The union holds bits only. Nothing inside it records which member was written, so the read reinterprets the bytes.

    Do this instead

    Keep a separate tag that records the active member and consult it before every read.

  • Common mistake

    Writing a new value without updating the tag

    What it looks like

    A write_int function that sets the union member but leaves kind at Kind::Double from an earlier write.

    Why it fails

    The tag now names a member that is no longer the active one, so later reads use the wrong interpretation.

    Do this instead

    Update the tag in the same function that performs the write, every time.

  • Common mistake

    Reading a member that does not match the record

    What it looks like

    Checking nothing and reading tv.value.d after the tag says Kind::Int.

    Why it fails

    The read interprets the bytes under a type that was not the one written, and the language does not report the mismatch.

    Do this instead

    Use a switch or an if check on the tag, and read only the member the tag names.

  • Common mistake

    Treating the tag as optional because the code currently works

Practice: Tracking the Active Member

EASY

Using the TaggedValue program from this article, add a third write function that stores an int value, then trace the output of this sequence: write_double(tv, 1.25), write_int(tv, 7), print(tv). Before you run it, state the value printed and the value of kind after each call. Then explain which part of the program decided what print showed.

Hints
  • Each write function changes both the union member and the tag.
  • print only reads the member that the tag names at the time it is called.
  • After write_int(tv, 7), the tag should be Kind::Int.

Summary of Tracking Responsibility

  1. 1
    A union holds a value of one of its member types at a time, and the language does not record which type is held.
  2. 2
    The programmer is responsible for tracking the active type, usually with a separate tag stored beside the union.
  3. 3
    Every write must update the tag, and every read must consult the tag before choosing a member.
  4. 4
    Forgetting to update the tag, or reading a member that does not match it, reinterprets the stored bits as the wrong type.

At a glance

  • A union holds a value of one of its member types at a time, and the language does not record which type is held.
  • The programmer is responsible for tracking the active type, usually with a separate tag stored beside the union.
  • Every write must update the tag, and every read must consult the tag before choosing a member.
  • Forgetting to update the tag, or reading a member that does not match it, reinterprets the stored bits as the wrong type.

Related concepts

More concepts starting with U