
One of the most subtle and frequently misunderstood topics in embedded C programming is the interaction between type punning and the strict aliasing rule. These low-level mechanisms govern how the compiler treats memory accesses through pointers of different types, and misunderstanding them can lead to subtle bugs, broken optimizations, or undefined behavior in production firmware. For embedded engineers working close to hardware—parsing peripheral registers, implementing communication protocols, or optimizing memory usage—a solid grasp of these concepts is not optional; it is essential.
In this article, we explore what the strict aliasing rule actually permits, when type punning is safe, and how to write portable code that compiles correctly under aggressive optimization levels commonly used in embedded toolchains.
The C standard’s strict aliasing rule, defined in C99 section 6.5 paragraph 7, states that an object shall have its stored value accessed only by an lvalue expression of a compatible type (with exceptions such as character types, which may alias any object representation). In simple terms, if you have a float variable, you should not access it through an int* pointer—doing so invokes undefined behavior.
Why does this rule exist? Compilers use type-based alias analysis to determine whether two pointers might refer to the same memory location. If the compiler can prove that pointers of unrelated types do not alias, it can reorder reads and writes for better performance—critical for achieving tight loop performance on resource-constrained microcontrollers.
void process_values(int *a, float *b) {*a = 10;*b = 3.14f;printf("%d\n", *a); // Compiler may cache *a in a register}
In this function, the compiler knows that int* and float* are unrelated types, so it can assume that writing to *b does not affect *a. It may cache the value of *a before the write to *b, even if both pointers happen to point to the same address. This optimization is valid only because of the strict aliasing rule.
Despite the rule, type punning is widespread in embedded firmware. Consider these typical use cases:
// Reading a 32-bit peripheral register as individual bytesuint32_t register_value = *((volatile uint32_t*)0x40021000);uint8_t low_byte = register_value & 0xFF;uint8_t high_byte = (register_value >> 8) & 0xFF;
This approach is safe because the CPU performs the type conversion through explicit bit manipulation rather than through pointer casting.
// Parsing received data from a UART bufferuint8_t rx_buffer[4];// ... receive data into rx_buffer ...uint32_t sensor_value = (uint32_t)rx_buffer[0]| ((uint32_t)rx_buffer[1] << 8)| ((uint32_t)rx_buffer[2] << 16)| ((uint32_t)rx_buffer[3] << 24);
The most common (and controversial) pattern in embedded code uses unions:
typedef union {uint32_t as_uint32;float as_float;uint8_t as_bytes[4];} converter_t;float bytes_to_f32(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) {converter_t c;c.as_uint32 = ((uint32_t)b0 << 24)| ((uint32_t)b1 << 16)| ((uint32_t)b2 << 8)| (uint32_t)b3;return c.as_float;}
Important note: Using unions for type punning is widely supported by most compilers (including GCC, Clang, and ARM compilers), but it is implementation-defined rather than strictly guaranteed by the C standard. In C++, it is undefined behavior. Since most embedded compilers support both languages differently, always check your compiler’s documentation.
__attribute__((may_alias)) and __attribute__((packed)) ApproachGCC and Clang provide attributes that give you explicit control over aliasing behavior:
typedef uint32_t __attribute__((may_alias)) aliased_uint32_t;void process_buffer(aliased_uint32_t *data, size_t count) {for (size_t i = 0; i < count; i++) {data[i] = data[i] ^ 0xDEADBEEF;}}
The may_alias attribute tells the compiler that this pointer may alias with pointers of any other type, preventing aggressive optimization from breaking your code.
memcpy Approach: Portable and SafeThe most portable and standards-compliant way to perform type punning is using memcpy. Modern compilers typically optimize small memcpy calls into direct register moves when the size is known at compile time, resulting in zero or negligible runtime overhead:
#include <string.h>float uint32_to_float(uint32_t value) {float result;memcpy(&result, &value, sizeof(float));return result;}uint32_t float_to_uint32(float value) {uint32_t result;memcpy(&result, &value, sizeof(uint32_t));return result;}
This approach is guaranteed well-defined in both C and C++, and GCC, Clang, IAR, and ARM Compiler all optimize the memcpy into a direct register move when the size is small and known at compile time.
On ARM Cortex-M processors, the difference between correct and incorrect aliasing handling can be significant. When the compiler can assume no aliasing, it keeps values in registers across loop iterations, reducing memory accesses. With incorrect aliasing assumptions, the compiler generates correct but suboptimal code—extra load and store instructions that waste cycles and increase power consumption.
Consider a typical DSP loop:
void process_samples(int16_t *output, const int16_t *input, size_t n) {for (size_t i = 0; i < n; i++) {int32_t acc = (int32_t)input[i] * 0x7FFF;output[i] = (int16_t)(acc >> 15);}}
If output and input could point to overlapping memory, the compiler must reload input[i] after every write to output[i]. Using the C99 restrict qualifier tells the compiler (by programmer guarantee) that the pointers do not overlap:
void process_samples(int16_t * restrict output,const int16_t * restrict input,size_t n) {// Compiler can now safely keep input[i] in a register}
This single qualifier can yield 20–40% performance improvement on tight DSP loops by enabling more efficient register allocation.
memcpy for type punning—it is portable, well-defined, and optimized away by modern compilersconst and restrict qualifiers on pointer parameters to give the compiler maximum optimization freedomunion-based punning only when you control the compiler and have verified the behaviorvolatile pointers of the correct type rather than through type-punned accesses-fno-strict-aliasing as a global workaround—it disables beneficial optimizations throughout your entire codebase instead of fixing the root cause-O2 or -O3The strict aliasing rule exists to enable better compiler optimization, but it can catch embedded engineers off guard when working with raw memory, peripheral registers, and protocol buffers. By understanding which forms of type punning are well-defined and which invoke undefined behavior, you can write firmware that is both correct and performant. Prefer memcpy for portability, use restrict to unlock optimization opportunities, and always verify aliasing-sensitive code under your target optimization level. These practices separate reliable firmware from code that only works by accident.
Quick Links
Legal Stuff




