What is the strict aliasing rule and why might your C++ code suddenly break?
\nHave you ever written seemingly perfectly correct C++ code, only to have the compiler at high optimization levels (-O2 or -O3) turn it into something non-functional? Welcome to the world of strict aliasing (strict aliasing rule). This is one of the most insidious rules of the language, capable of turning debugging into a real quest. At PythonLib, we love explaining complex topics in simple terms, and today's article is no exception.
Let's imagine: you have two pointers of different types pointing to the same memory location. The compiler, following the strict aliasing rule, has every right to decide that they cannot refer to the same object. And if you violate this rule — expect undefined behavior (UB). The program might work, it might crash, or it might produce incorrect results. Sounds scary? Let's figure it out.
\n\nThe essence of the rule: why is the compiler so "strict"?
\nThe strict aliasing rule in C++ (and C) states: accessing an object through a pointer or reference of an incompatible type is UB. Exceptions exist only for char, unsigned char, and std::byte. Why is this needed? The answer is simple: optimization. If the compiler is certain that two pointers of different types cannot point to the same memory, it can safely reorder operations, cache values in registers, and aggressively optimize code. Without this rule, the performance of many programs would drop significantly.
But there is a downside: developers who try to perform type punning (interpreting the same bytes as different types) often step on a rake. A classic example is trying to quickly check if a floating-point number is NaN via a reinterpret_cast to int. The compiler can "break" such code.
\n\nWhat does a strict aliasing violation look like? Code examples
\nLet's look at the most popular example, which travels from one Stack Overflow to another:
\n#include <iostream>\n#include <cstdint>\n\nint main() {\n float f = 3.14f;\n // Strict aliasing violation: reading a float as an int\n int* pi = reinterpret_cast<int*>(&f);\n std::cout << *pi; // UB!\n return 0;\n}\n\nAt first glance — a common trick for viewing the bit representation of a float. But the C++ standard says: float and int are incompatible types. The compiler can optimize this code so that you get garbage. With optimizations enabled, GCC, for example, might simply return some predetermined value or even remove the code entirely.
Another example — working with byte arrays:
\nstruct Point { int x, y; };\nchar buffer[sizeof(Point)];\n// Fill buffer...\nPoint* p = reinterpret_cast<Point*>(buffer);\np->x = 42; // UB, if buffer was not created as a Point\n\nHere, the violation occurs because buffer is a char array, and we are accessing it as a Point. Although char* can point to any object, the reverse is not true: Point* cannot point to char[] unless the memory was allocated as a Point.
Comparison of approaches: how to safely circumvent strict aliasing?
\nThere are several ways to legally perform type punning in C++. We will compare three main approaches: memcpy, union, and std::bit_cast (C++20). Each has its pros and cons.
1. memcpy — the good old way
\nThe safest and most portable method is to copy bytes via memcpy. Compilers optimize this call excellently, often turning it into a simple register load, so there is no overhead:
#inc