Why doesn’t [[msvc::no_unique_address]] work like [[no_unique_address]] here?

Summary

A class hierarchy using [[msvc::no_unique_address]] fails to compile on MSVC when multiple empty members with this attribute appear across base and derived classes. The behavior differs from standard [[no_unique_address]], leading to confusion. The failure is intentional, rooted in MSVC’s ABI rules and its more restrictive interpretation of empty-base and empty-member optimizations.

Root Cause

MSVC’s [[msvc::no_unique_address]] is not equivalent to standard [[no_unique_address]]. It is an MSVC‑specific extension with stricter constraints:

  • MSVC does not allow overlapping storage for empty members across inheritance boundaries.
  • The compiler enforces unique storage slots for each such member when used in a derived class.
  • This causes layout conflicts when the same pattern appears repeatedly in a class hierarchy.
  • The conflict triggers a compile‑time diagnostic rather than silently producing a layout MSVC considers unsafe.

Why This Happens in Real Systems

Compilers must maintain a stable ABI. MSVC’s ABI has long‑standing rules:

  • Empty base optimization is limited compared to GCC/Clang.
  • Empty member optimization is even more constrained.
  • Allowing overlapping empty members across inheritance could break:
    • object identity rules
    • pointer adjustment logic
    • debugger expectations
    • binary compatibility with existing MSVC‑compiled libraries

Because of these constraints, MSVC chooses safety and ABI stability over aggressive optimization.

Real-World Impact

This design choice affects real codebases in several ways:

  • Portability issues when code relies on GCC/Clang behavior.
  • Unexpected layout differences between compilers.
  • Compile‑time failures when patterns rely on repeated empty members.
  • ABI mismatches in cross‑compiler builds.
  • Template metaprogramming breakage when using empty tag types or EBO tricks.

Example or Code (if necessary and relevant)

The user’s example is valid C++20, but MSVC rejects it because the repeated empty members with [[msvc::no_unique_address]] violate its layout rules:

#define NO_UNIQUE_ADDRESS [[no_unique_address]] [[msvc::no_unique_address]]

struct S1 { struct {} e NO_UNIQUE_ADDRESS; };
struct S2 : S1 { struct {} e NO_UNIQUE_ADDRESS; };
struct S3 : S2 { struct {} e NO_UNIQUE_ADDRESS; };

int main() {
    static_assert(sizeof(S3) == sizeof(S1));
}

How Senior Engineers Fix It

Experienced engineers avoid relying on MSVC’s extension for cross‑compiler layout tricks. They typically:

  • Use only standard [[no_unique_address]] when portability matters.
  • Avoid repeating empty members across inheritance layers.
  • Replace empty tag types with:
    • std::integral_constant
    • enumerations
    • static constexpr flags
  • Encapsulate layout‑sensitive code behind compiler‑specific branches.
  • Use composition instead of inheritance when layout must be predictable.

Key takeaway: If you need portable empty‑member optimization, rely on the standard attribute, not the MSVC extension.

Why Juniors Miss It

Less experienced developers often assume:

  • All compilers implement attributes identically.
  • Vendor extensions behave like standard features.
  • Empty types are always optimized away.
  • Inheritance does not affect empty‑member layout.

They also tend to test only on one compiler, missing subtle ABI constraints until integration time.

The subtlety: MSVC’s attribute is not a drop‑in replacement for the standard one, and its ABI rules are far more conservative.

Leave a Comment