Skip to content
C++
Language
Advanced

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++98

The 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, or std::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_cast itself 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 restrict is a separate, programmer-asserted no-alias promise. C++ has no standard restrict; compilers offer __restrict as an extension.

Syntax

There is no dedicated syntax. The rule applies to every access, and the following are the sanctioned ways to reinterpret bytes:

cpp
#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

cpp
#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

cpp
#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.

cpp
#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:

cpp
#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

cpp
#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

cpp
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 in constexpr.
  • Use memcpy for pre-C++20 code and for buffer-to-object conversions. Modern compilers generate the same code as a cast.
  • Use std::byte (C++17) or unsigned char for 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 alignas on buffers (see alignas).
  • Use std::span<std::byte> (C++20) to express byte-oriented interfaces.
  • Test with -fsanitize=undefined and compile with -Wstrict-aliasing=2 (GCC). Its coverage is incomplete, so don't rely on it alone.
  • Treat -fno-strict-aliasing as 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_cast instead.
  • char* is special in only one direction. You may read any object via char*, but you may not treat a char array as storage for an int and read it through int* without an object having been created there.
  • Signed/unsigned is the only integer exemption. int and unsigned are compatible. int and long, or int and float, are not, even when the sizes match. int32_t and float are 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_t is usually unsigned char, but the standard doesn't guarantee that. Prefer unsigned char or std::byte explicitly.
  • 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 __m128 are declared may_alias in GCC and Clang. Your own vector typedefs are not, so use the provided load/store intrinsics or memcpy.

See Also