Hello! Welcome back to our series on C pointers.
In our last lesson, we saw how to use opaque pointers to hide implementation details and build robust, stable APIs. That was about controlling what the programmer can see. Today, we're diving into a subtler but equally critical topic: controlling what the compiler can see, or rather, what it's allowed to assume about your pointers.
This lesson addresses the learning outcome: Explain pointer aliasing and the implications of violating the strict aliasing rule.
Understanding this rule is the difference between writing code that happens to work and writing code that is provably correct and efficient. For someone in your field, where performance and correctness under aggressive compiler optimization are non-negotiable, this is a cornerstone concept.
What is Pointer Aliasing?
At its core, pointer aliasing is simple: it's the situation where two or more different pointers refer to the same memory location.

The Problem: How Aliasing Handcuffs the Compiler
Why do we care if pointers alias? Because the possibility of aliasing can prevent the compiler from making powerful optimizations. The compiler must be conservative. If two pointers could alias, the compiler must assume they do.
Let's see a concrete example.
Pointers in C, Part III: The Strict Aliasing Rule
The article 'Pointers in C, Part III: The Strict Aliasing Rule' provides a perfect introductory example. It shows how pointer aliasing prevents a simple optimization.
Read the section titled 'POINTER ALIASING DEFINED'. Pay close attention to the silly(int* x, int* y) function. Notice how the author explains that the compiler cannot simply return 0, because x and y might be aliased. The assembly output provides clear proof that a memory reload is required.
As the article demonstrates, because x and y could point to the same integer, the write *y = 1; might have changed the value of *x. Therefore, the compiler is forced to generate code to reload the value of *x from memory before returning, even though it just stored a 0 there.
This is not just a theoretical concern. The performance implications are significant. When the compiler cannot prove that pointers don't alias, it may be prevented from performing a host of optimizations.
Aliasing in C++ - Risks, Opportunities and Techniques - Roi Barkan - CppCon 2022
This talk, 'Aliasing in C++' by Roi Barkan, while C++ focused, perfectly explains the performance impact. The principles are identical for C.
Watch the segment from 18:04 to 22:57. The speaker explains three key optimizations that aliasing can block: Register Allocation: Keeping variables in fast CPU registers instead of slow memory. Vectorization: Using SIMD instructions to perform operations on multiple data points at once. Expression Hoisting: Moving a calculation (like sin(coefficient)) out of a loop. Notice the dramatic performance difference (32x!) when the compiler can prove aliasing won't occur.
The Compiler's Escape Clause: The Strict Aliasing Rule
The C standard provides a way out of this performance trap: the Strict Aliasing Rule (SAR).
In essence, the SAR is a promise from you, the programmer, to the compiler. It states: pointers to incompatible types will never point to the same memory location.
By making this a rule, the language allows the compiler to assume that if it sees an access through a float* and an access through an int*, they cannot be referring to the same memory. This unlocks the optimizations we just discussed.
Violating this rule results in Undefined Behavior (UB). This doesn't mean your code will crash; it means the compiler is free to generate code that does something completely unexpected, because it operated on an assumption you told it was safe.
Advanced C: The UB and optimizations that trick good programmers.
To understand the mindset behind this, let's watch a clip from 'Advanced C: The UB and optimizations that trick good programmers'. This video brilliantly frames Undefined Behavior not as a mistake, but as a deliberate feature that enables performance.
Watch the following segments: 18:59 - 20:53: An introduction to Undefined Behavior (UB) and why it's powerful for compilers. 46:26 - 50:15: The speaker introduces the memory model and the aliasing problem with a concrete memset example. 50:15 - 53:34: This part directly explains type aliasing (the strict aliasing rule) and its exceptions. Pay attention to the union example, which is a classic violation.
A Classic Violation
Let's look at one of the most common ways the strict aliasing rule is violated, especially in systems programming, networking, or driver development: type punning via pointer casting. This is when you reinterpret a region of memory as a different type.
Imagine you have an integer, but you want to treat it as a float. A naive approach would be:
int i = 1078523331; // Some integer bit pattern
float* f_ptr = (float*)&i;
// ... later ...
printf("%f\n", *f_ptr); // VIOLATION!
Here, you've created an int* and a float* that alias the same memory. This is a direct violation of the strict aliasing rule. The compiler is allowed to assume that any write to a float cannot affect i, and any write to an int cannot affect what f_ptr points to.
Let's see how this can break your code in a subtle way.
What is the strict aliasing rule?
This Stack Overflow answer provides a fantastic, detailed breakdown of a strict aliasing violation, complete with compiler output.
Read the sections 'What is strict aliasing?' and 'Preliminary examples'. Focus on the foo(float *f, int *i) function. Notice that when called with both pointers pointing to the same memory location, the optimized code returns 1 instead of 0. The explanation and the generated assembly clearly show the compiler assuming that the write *f = 0.f; could not possibly affect the value of *i, so it optimizes away the final read of *i.
Test your understanding!
Consider the following function:
void process_data(long* data, int* flag) {
*data = 100;
*flag = 0;
// Some complex logic here...
if (*data == 100) {
// Do something important
printf("Data is 100\n");
} else {
// Handle unexpected change
printf("Data is %ld\n", *data);
}
}
What would you expect to be printed if you call this function as follows?process_data((long*)&some_int_var, &some_int_var);
Assume long is 64-bit and int is 32-bit. How might an optimizing compiler behave differently from a non-optimizing one?
Show answer
In this scenario, data (a long*) and flag (an int*) are aliasing the same memory location, some_int_var. This is a violation of the strict aliasing rule.
-
Without Optimization: The code will likely behave sequentially.
*data = 100;writes 8 bytes (value 100) tosome_int_var.*flag = 0;writes 4 bytes (value 0) to the same location, overwriting the lower 4 bytes of the100value.if (*data == 100)re-reads the value from memory. Since the value was just changed to0, this condition will be false.- The output will likely be:
Data is 0
-
With Optimization: An optimizing compiler, abiding by the strict aliasing rule, will assume that
dataandflagcannot possibly alias because they are of incompatible types.- It sees
*data = 100;. - It sees
*flag = 0;and assumes this write cannot affect the memory pointed to bydata. - When it reaches
if (*data == 100), the compiler reasons: "I just stored 100 in*data, and the only other write was through*flag, which can't affect*data. Therefore,*datamust still be 100." - The compiler can optimize the check away and assume the
ifcondition is always true. - The output will likely be:
Data is 100
- It sees
This demonstrates how a strict aliasing violation leads to UB: the optimized code's behavior diverges from the sequential execution you might expect, because the compiler made a "legal" but incorrect assumption based on the rule.
The Rules of Engagement: What Is Allowed?
So, which types are allowed to alias? The C standard carves out specific exceptions. Accessing an object is legal through a pointer of a different type if that type is:
- A compatible type (e.g.,
intandint). - A qualified version of a compatible type (e.g.,
intandconst int). - A corresponding signed or unsigned type (e.g.,
intandunsigned int). - An aggregate type (a
structorunion) that contains one of the above types as a member. - A character type (
char,signed char, orunsigned char).
The last one is the most important exception. A char* can point to and access any object, byte by byte. This is why functions like memcpy and memset, which operate on raw memory using char* (or void* which is typically cast to char*), are legal.
How to Type Pun Correctly
If casting pointers is illegal, how can you safely reinterpret memory, a common need in your work with communication protocols and hardware registers?
The standard-compliant and safe method is memcpy.
int i = 1078523331;
float f;
// Copy the bit pattern of 'i' into 'f'.
memcpy(&f, &i, sizeof(f));
printf("%f\n", f); // This is now safe and well-defined.
This might seem inefficient, but modern compilers are smart. When memcpy is used with a known size, they recognize this pattern and will optimize it down to a single register move or a direct memory load/store—exactly what the unsafe pointer cast was trying to achieve, but in a well-defined way.
What is the strict aliasing rule?
The same Stack Overflow answer clearly explains this correct approach.
Read the sections 'What is Type Punning' and 'How do we Type Pun correctly?'. This section explains why the traditional cast method is UB and shows that memcpy is the correct, standard-compliant solution that good compilers optimize perfectly.
Pragmatic Summary
The strict aliasing rule is a complex topic, but the practical guidance is straightforward.
Pointers in C, Part VII: Being Relaxed About The Strict Aliasing Rule
The article 'Being Relaxed About The Strict Aliasing Rule' offers an excellent, pragmatic summary for working developers.
Read the final section 'NO PUN INTENDED'. It provides a fantastic set of pragmatic rules for dealing with SAR in real-world codebases.
Conclusion
Today we've peeled back a layer of the C language and looked at the contract between the programmer and the compiler. The Strict Aliasing Rule is a key part of that contract, enabling aggressive optimizations that are vital for high-performance code.
Key Takeaways:
- Pointer Aliasing: Occurs when multiple pointers refer to the same memory. Its possibility forces compilers to be conservative, hurting performance.
- Strict Aliasing Rule (SAR): The compiler is allowed to assume that pointers to incompatible types do not alias. Violating this rule is Undefined Behavior.
- Common Violation: Using pointer casts to "type pun" (e.g.,
float* fp = (float*)&some_int;) is a direct violation and can lead to optimized code that reads stale values from registers. - The Big Exception: A
char*can legally alias any other type, allowing for byte-level memory manipulation. - The Correct Solution: The safe, portable, and well-defined way to type pun is to use
memcpy. Compilers will optimize this into the most efficient instruction possible.
Preview of the Next Lesson:
We've seen that memcpy is the correct way to type pun and that pointer casting is not. However, there is another tool often used for this purpose: union. In the next lesson, we will use unions with pointers for type punning, and describe its use in embedded systems, exploring both its power and its own set of C/C++ standard-related pitfalls.