Strict Aliasing
The rule that restricts which lvalue types may access an object, enabling type-based alias analysis, and the safe alternatives to type punning.
Strict Aliasingsince C++98The strict aliasing rule ([basic.lval], "glvalue access rules") makes it undefined behavior to access an object through a glvalue whose type is not similar to the object's dynamic type, unless the access type is a signed/unsigned variant, a base class, char, unsigned char, or std::byte (the latter since C++17).
Overview
The rule exists so that compilers can perform type-based alias analysis (TBAA): a store through a float* is assumed not to modify an int object, so the compiler may keep values in registers, reorder loads and stores, and vectorize loops without reloading memory after every write.
An object may be accessed only through a glvalue of one of these types:
- The object's own dynamic type (ignoring cv-qualifiers).
- A type similar to it (differing only in cv-qualification at pointer levels).
- The signed or unsigned counterpart of the object's type.
- An aggregate or union type that holds the type among its members (recursively).
- A base class of the object's type.
char,unsigned char, orstd::byte(C++17). These may inspect any object's bytes.
Violating the rule is undefined behavior even if the code "works" at -O0. The problem usually shows up only after optimization (-O2, LTO), which makes it hard to debug. GCC and Clang enable -fstrict-aliasing at -O2 and above. MSVC does not implement TBAA in the same way, so code that breaks the rule can behave differently across compilers.
Related facts that are easy to confuse:
reinterpret_castitself is not the violation. The access through the resulting pointer is.- C permits union type punning, but C++ does not. Reading an inactive union member is undefined behavior, although GCC and Clang document it as an extension.
- C99's
restrictis a separate, programmer-asserted no-alias promise. C++ has no standardrestrict; compilers offer__restrictas an extension.
Syntax
There is no dedicated syntax. The rule applies to every access, and the following are the sanctioned ways to reinterpret bytes:
#include <cstring>
#include <bit> // std::bit_cast (C++20)
#include <cstdint>
float f = 1.0f;
// 1. memcpy: valid in all standards
std::uint32_t a;
std::memcpy(&a, &f, sizeof a);
// 2. std::bit_cast: since C++20, constexpr-capable
auto b = std::bit_cast<std::uint32_t>(f); // C++20
// 3. Byte-wise inspection: valid via unsigned char (C++98) / std::byte (C++17)
const auto* bytes = reinterpret_cast<const unsigned char*>(&f);Examples
The classic violation
#include <cstdint>
std::uint32_t bits_of(float f) {
return *reinterpret_cast<std::uint32_t*>(&f); // UB: float accessed as uint32_t
}At -O2 the compiler may assume the uint32_t load cannot refer to the float, so it may read uninitialized or stale storage.
The correct fix
#include <bit>
#include <cstdint>
#include <cstring>
// C++20
constexpr std::uint32_t bits_of(float f) noexcept {
static_assert(sizeof(float) == sizeof(std::uint32_t));
return std::bit_cast<std::uint32_t>(f);
}
// Pre-C++20 fallback (not constexpr)
inline std::uint32_t bits_of_legacy(float f) noexcept {
std::uint32_t u;
std::memcpy(&u, &f, sizeof u); // compilers lower this to a single register move
return u;
}Parsing a network buffer
A frequent real-world violation is overlaying a struct on a received byte buffer.
#include <cstdint>
#include <cstring>
#include <span> // C++20
#include <optional>
struct Header {
std::uint16_t type;
std::uint16_t length;
std::uint32_t seq;
};
// Wrong: UB (strict aliasing, and possibly alignment)
// const Header* h = reinterpret_cast<const Header*>(buf.data());
// Right: copy into a properly typed object
std::optional<Header> parse(std::span<const std::byte> buf) { // C++20
if (buf.size() < sizeof(Header)) return std::nullopt;
Header h;
std::memcpy(&h, buf.data(), sizeof h);
// convert byte order here if the wire format differs from the host's
return h;
}Since Header is trivially copyable, memcpy into it is well-defined and optimizers elide the copy when they can.
Implicit-lifetime types (C++20)
Since C++20 (applied retroactively as a defect report to earlier modes by most compilers), operations such as std::malloc, operator new, and std::memcpy into storage can implicitly create objects of implicit-lifetime types (trivial types and aggregates). This legitimizes some buffer reuse patterns. Since C++23, std::start_lifetime_as<T>() makes the intent explicit:
#include <memory> // std::start_lifetime_as: C++23
#include <cstddef>
// C++23: no copy, begins the lifetime of an implicit-lifetime object in existing storage
const Header* view(const std::byte* p) {
return std::start_lifetime_as<Header>(p);
}Compiler support for std::start_lifetime_as has lagged, so check your toolchain before depending on it. Alignment of p must still satisfy alignof(Header).
Placement new and launder
#include <new>
#include <cstddef>
alignas(int) std::byte storage[sizeof(int)];
int* make() {
return new (storage) int(42); // begins lifetime of an int; valid
}
// Reading storage through unsigned char / std::byte remains valid.
// std::launder (C++17) is needed when reusing storage whose
// previous object was const or contains const/reference members.Aliasing and optimization
void scale(float* out, const int* count, float k) {
for (int i = 0; i < *count; ++i) // *count can be hoisted: an int is not a float
out[i] *= k;
}With float* and int* the compiler can load *count once. If count were also float* or char*, it must assume out[i] *= k might change it and reload every iteration.
Best Practices
- Prefer
std::bit_cast(C++20) for value-to-value reinterpretation. It checks size and trivial copyability at compile time and works inconstexpr. - Use
memcpyfor pre-C++20 code and for buffer-to-object conversions. Modern compilers generate the same code as a cast. - Use
std::byte(C++17) orunsigned charfor raw storage, and keep typed access behind accessor functions. - Check alignment as well as aliasing. Strict aliasing and alignment are separate rules, and violating either is UB. Use
alignason buffers (see alignas). - Use
std::span<std::byte>(C++20) to express byte-oriented interfaces. - Test with
-fsanitize=undefinedand compile with-Wstrict-aliasing=2(GCC). Its coverage is incomplete, so don't rely on it alone. - Treat
-fno-strict-aliasingas a mitigation for legacy code (the Linux kernel uses it), not as a fix. It makes the build non-portable in behavior and costs optimization.
Common Pitfalls
- Union punning. Writing one member and reading another is UB in C++, whatever C allows. GCC and Clang tolerate it, but MSVC and standard-conforming reasoning do not guarantee it. Use
bit_castinstead. char*is special in only one direction. You may read any object viachar*, but you may not treat achararray as storage for anintand read it throughint*without an object having been created there.- Signed/unsigned is the only integer exemption.
intandunsignedare compatible.intandlong, orintandfloat, are not, even when the sizes match.int32_tandfloatare never compatible. - Casting between unrelated struct pointers. Two structs with identical layouts are still different types, so access through the wrong one is UB.
- Aliasing through
uint8_t.uint8_tis usuallyunsigned char, but the standard doesn't guarantee that. Preferunsigned charorstd::byteexplicitly. - Hidden violations across translation units. Inlining and LTO make an access that was safe in separate TUs unsafe once the compiler sees both sides.
- Assuming it works because tests pass. A different compiler version, flag, or inlining decision can change the result.
- Type-punned SIMD intrinsics. Intrinsic vector types such as
__m128are declaredmay_aliasin GCC and Clang. Your own vector typedefs are not, so use the provided load/store intrinsics ormemcpy.