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.
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
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
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.
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.
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.
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.
#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);
}- 1The struct TaggedValue pairs the tag kind with the union value.
- 2
write_intstores 42 in member i and sets kind to Int. - 3print sees Kind::Int and reads member i, printing 42.
- 4
write_doublereplaces the stored value with 3.5 in member d and sets kind to Double. - 5print 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
empty
42
3.5Reading 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.
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.
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 likeWriting an int and then reading the double member, expecting the int value back.
Why it failsThe union holds bits only. Nothing inside it records which member was written, so the read reinterprets the bytes.
Do this insteadKeep 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 likeA write_int function that sets the union member but leaves kind at Kind::Double from an earlier write.
Why it failsThe tag now names a member that is no longer the active one, so later reads use the wrong interpretation.
Do this insteadUpdate 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 likeChecking nothing and reading tv.value.d after the tag says Kind::Int.
Why it failsThe read interprets the bytes under a type that was not the one written, and the language does not report the mismatch.
Do this insteadUse 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
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
- 1A union holds a value of one of its member types at a time, and the language does not record which type is held.
- 2The programmer is responsible for tracking the active type, usually with a separate tag stored beside the union.
- 3Every write must update the tag, and every read must consult the tag before choosing a member.
- 4Forgetting 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.