Summary
This incident examines why multiple [[no_unique_address]] fields of the same empty type do not overlap across a class hierarchy, even though the attribute suggests that empty subobjects may share storage. The expectation that S3 should be the same size as S1 is reasonable at first glance, but the C++ object model imposes strict rules that prevent such overlapping.
Root Cause
The root cause is that empty base class subobjects and empty member subobjects of the same type must remain distinct objects in the C++ object model.
[[no_unique_address]] allows the compiler to reuse storage, but only when doing so does not violate object identity rules.
Key constraints that prevent overlap:
- Each subobject of type
Emust have a unique address when its address is taken. - The standard requires that distinct subobjects are not allowed to have the same address if they are of the same type and appear in a way that would make them indistinguishable.
- Inheritance introduces base class subobjects, which must remain structurally identifiable.
[[no_unique_address]]is permitted, not guaranteed; compilers must preserve object identity semantics.
Why This Happens in Real Systems
Real compilers must obey the C++ object model, which enforces:
- Unique object identity: Two different subobjects of the same type must be distinguishable.
- Pointer-to-member rules: Taking the address of
S1::e,S2::e, andS3::emust yield distinct, valid pointers. - Layout rules for inheritance: Base class subobjects must be laid out in a way that preserves their identity and ordering.
If the compiler overlapped all E subobjects:
&static_cast<S1&>(s3).e&static_cast<S2&>(s3).e&s3.e
would all be the same address, violating the requirement that these are distinct subobjects.
Real-World Impact
This behavior affects:
- CRTP patterns where empty tag types are used repeatedly
- policy-based design with many empty policy classes
- embedded systems where developers aggressively optimize object size
- ABI stability, since compilers cannot break layout guarantees
Practical consequences:
- Structs may be larger than expected
- Inheritance chains accumulate empty subobjects
[[no_unique_address]]cannot collapse identity-required subobjects
Example or Code (if necessary and relevant)
Below is a minimal reproduction showing why the compiler cannot legally overlap the objects:
struct E {};
struct S1 { [[no_unique_address]] E e; };
struct S2 : S1 { [[no_unique_address]] E e; };
struct S3 : S2 { [[no_unique_address]] E e; };
int main() {
S3 s;
// These must be distinct objects:
auto p1 = &static_cast(s).e;
auto p2 = &static_cast(s).e;
auto p3 = &s.e;
}
If the compiler overlapped them, p1, p2, and p3 would be identical, violating the standard.
How Senior Engineers Fix It
Experienced engineers recognize that object identity rules dominate layout optimizations. They typically:
- Avoid relying on
[[no_unique_address]]across inheritance hierarchies - Use composition instead of inheritance when optimizing size
- Consolidate empty marker types into a single shared instance
- Replace empty types with enum tags or constexpr values when possible
- Use
[[no_unique_address]]only for independent empty members, not repeated ones of the same type
Why Juniors Miss It
Less experienced developers often assume:
[[no_unique_address]]is a guarantee, not a permission- Empty types can always be collapsed without consequence
- Inheritance does not impose additional layout constraints
- Object identity rules are “just theoretical” and won’t affect real code
The subtlety is that C++ prioritizes object identity over storage reuse, and [[no_unique_address]] cannot override that requirement.