Hello! Welcome back to our series on advanced C concepts for your interview preparation.
In our last lesson, we explored bit-fields, focusing on how their layout is often implementation-defined, which creates portability challenges. We saw that they are a tool for controlling the memory representation of data at the bit level.
Today, we'll examine another powerful tool for data representation: the union. Our focus will be on using unions for type-punning—a technique to reinterpret the memory of one data type as another, such as accessing the individual bytes of a uint32_t. While this is extremely useful in low-level programming, it's also a minefield of portability issues and undefined behavior. We'll dissect why this is, focusing on the strict aliasing rule, which is a common topic in senior-level C/C++ interviews.
1. The Core Concept of a Union
Unlike a struct, where each member has its own separate memory location, all members of a union share the same memory location. The size of the union is determined by the size of its largest member.
This fundamental property is what enables type-punning.

Let's start with a practical cheat sheet that's popular in the embedded world. It gives a concise overview of how unions are used.
Unions - Embedded C Programming cheat sheet
The article "Unions - Embedded C Programming cheat sheet" from EWskills provides a quick, practical introduction to unions from a firmware perspective.
Please read the introduction and the section titled "Struct vs Union — Key Difference". This will quickly establish the basic definition and contrast it with structs, which you're already very familiar with.
2. Type-Punning: Reinterpreting Memory
Type-punning is the act of using a union to write to one member and then read from another. This allows you to inspect the raw byte representation of a type without any explicit casting. Given your background in AUTOSAR and communication stacks (CAN, SOME/IP), you've likely encountered this pattern for serializing data for transmission or deserializing received byte streams.
Let's look at the common patterns shown in the same resource.
Unions - Embedded C Programming cheat sheet
Let's continue with the EWskills article to see concrete examples of type-punning.
Read the sections "Why Use Unions in Firmware?", "Key Concept: Memory Reinterpretation", and "Common Union Patterns in Firmware". Pay close attention to the example of extracting bytes from a 32-bit value.
Let's work through a classic example. Imagine you need to send a uint32_t sensor reading over a CAN bus, which transmits data in byte-sized chunks.
#include <stdio.h>
#include <stdint.h>
// A union to reinterpret a 32-bit integer as an array of 4 bytes.
union Converter {
uint32_t word;
uint8_t bytes[4];
};
int main() {
union Converter c;
c.word = 0x12345678; // Write to the 32-bit member
printf("Word value: 0x%X\n", c.word);
// Now, read from the byte array member.
// The order of bytes depends on the system's endianness.
printf("Byte representation:\n");
for (int i = 0; i < 4; ++i) {
printf(" bytes[%d] = 0x%02X\n", i, c.bytes[i]);
}
return 0;
}
On a little-endian system (like x86), the least significant byte (LSB) is stored at the lowest memory address. The output would be:
Word value: 0x12345678
Byte representation:
bytes[0] = 0x78
bytes[1] = 0x56
bytes[2] = 0x34
bytes[3] = 0x12
On a big-endian system, the most significant byte (MSB) is stored first. The output would be different. This endianness dependency is one of the primary pitfalls and a classic interview question.
3. The Danger: The Strict Aliasing Rule
So, if type-punning works and is widely used, what's the problem? The danger lies in the Strict Aliasing Rule, a C/C++ standard rule that allows compilers to perform aggressive optimizations.
The rule essentially states that two pointers of different types cannot point to (or "alias") the same memory location, with a few exceptions (like char*).
When you write to one union member and read from another, you are effectively accessing the same memory through two different types. This can violate the compiler's assumptions and lead to Undefined Behavior (UB). Your code might work perfectly with optimizations disabled (-O0) but fail in unpredictable ways in a release build (-O2 or -O3).
To get an expert-level view on this, we'll consult a much more detailed resource.
Deep Dive into Unions in C and C++
The GitHub Gist "Deep Dive into Unions in C and C++" provides a nuanced and standard-aware explanation of the aliasing problem.
Please read section 3, "The big minefield: type punning, aliasing, and ‘reading inactive members’", focusing on 3.2 and 3.3. This will explain the controversy in C and introduce the strict aliasing rule as the root cause of the problem.
Why Optimizers Care About Aliasing
Consider this simplified function:
void process_data(float* f_val, int* status) {
*f_val = 3.14f; // Write to float
*status = 1; // Write to int
}
The compiler can assume that f_val and status point to different memory locations because they are different types. It might reorder these two writes for better performance, knowing one won't affect the other.
But what if you tricked the system with a union?
union {
float f;
int i;
} my_union;
process_data(&my_union.f, &my_union.i);
Now, both pointers alias the same memory. If the compiler reordered the writes, the final value in the union would depend on the reordering. This is a classic aliasing violation, and this is why type-punning through pointers to union members is dangerous.
C vs. C++: A Critical Distinction
The reading you just did highlights an important difference between C and C++ that you should be able to articulate in an interview:
- In C++: The standard is clear. Reading from a union member that was not the last one written to (the "inactive" member) is Undefined Behavior. Period.
- In C (C99 and later): The situation is more nuanced. The standard includes a special note (Technical Corrigendum 2, DR283) that is widely interpreted to allow type-punning as long as you access the memory through the union variable itself (e.g.,
my_union.bytes[0]) and not through pointers of different types that you've derived from it.
Even with the C exception, it's a complex and often debated area. For portable and safe code, especially in a mixed C/C++ environment like automotive software, it's best to avoid relying on this behavior.
4. The Standard-Compliant Way to Type-Pun
So, how can you achieve the goal of reinterpreting types without invoking Undefined Behavior? The recommended, portable, and optimizer-safe method is memcpy.
Deep Dive into Unions in C and C++
Let's return to the GitHub Gist to see the recommended safe patterns.
Read section 10.2 "Safe bit reinterpretation (C and pre-C++20): memcpy" and the summary in section 11, "Common misconceptions". This explains why memcpy is the correct tool for the job.
Using memcpy explicitly tells the compiler your intention: to copy the raw bit pattern from one location to another. The compiler understands this and won't make incorrect aliasing assumptions.
Here is our CAN bus example rewritten safely with memcpy:
#include <stdio.h>
#include <stdint.h>
#include <string.h> // For memcpy
int main() {
uint32_t word = 0x12345678;
uint8_t bytes[4];
// Safely copy the byte pattern from the integer to the byte array.
memcpy(bytes, &word, sizeof(word));
printf("Word value: 0x%X\n", word);
printf("Byte representation:\n");
for (int i = 0; i < 4; ++i) {
printf(" bytes[%d] = 0x%02X\n", i, bytes[i]);
}
return 0;
}
A common concern is performance. Won't a function call to memcpy be slow? Modern compilers are extremely smart. When memcpy is used with a small, known size, they will almost always optimize it away and replace it with a single, efficient machine instruction (e.g., a direct register move), resulting in zero performance overhead compared to the unsafe union-punning approach.
Conclusion
Today we've taken a deep dive into unions, a powerful but hazardous feature of C and C++. While commonly used for type-punning in low-level code, it's critical to understand the associated risks to write robust and portable software.
Key Takeaways for Your Interview:
- Unions and Type-Punning: Unions enable type-punning by allowing different typed members to share the same memory, which is useful for data serialization/deserialization.
- The Strict Aliasing Rule: This compiler optimization rule is the primary source of danger. It assumes pointers of different types don't alias the same memory. Union-based type-punning can violate this, leading to Undefined Behavior.
- C vs. C++: Be ready to explain that type-punning via inactive members is clearly UB in C++, while C has a complex exception that makes it conditionally legal but still risky.
- Portability Concerns: The result of type-punning is often dependent on system endianness.
- The Safe Alternative: The correct, portable, and optimizer-friendly way to reinterpret types is by using
memcpy. Modern compilers will optimize this efficiently.
In our next lesson, we'll shift our focus from how data is represented to how behavior can be parameterized. We will explore function pointers, covering their declaration, usage, and their role in implementing powerful software patterns like callbacks and dispatch tables, which are cornerstones of driver and middleware architectures like AUTOSAR.