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.