Description
Consider the following examples:
#include <cstddef>
struct S1 {};
struct S2 {};
struct S3 {
[[no_unique_address]] [[msvc::no_unique_address]] S1 s1;
};
struct ST1 {
[[no_unique_address]] [[msvc::no_unique_address]] S3 s1;
S1 s2;
};
struct ST2 {
[[no_unique_address]] [[msvc::no_unique_address]] S3 s1;
S2 s2;
};
struct ST3 {
[[no_unique_address]] [[msvc::no_unique_address]] S3 s1;
[[no_unique_address]] [[msvc::no_unique_address]] S1 s2;
};
struct ST4 {
[[no_unique_address]] [[msvc::no_unique_address]] S3 s1;
[[no_unique_address]] [[msvc::no_unique_address]] S2 s2;
};
static_assert(sizeof(ST1) == 2);
static_assert(offsetof(ST1,s2) != offsetof(ST1,s1.s1), "Subobjects of same type must not have same pointer");
size_t size_st1() {
return sizeof(ST1);
}
size_t offset_st1() {
return offsetof(ST1, s2);
}
// Failed by MSVC
static_assert(sizeof(ST2) == 1);
// Failed by MSVC
static_assert(offsetof(ST2,s2) == offsetof(ST2,s1.s1), "Subobjects of different types where at least one is zero-sized may have same pointer");
size_t size_st2() {
return sizeof(ST2);
}
size_t offset_st2() {
return offsetof(ST2, s2);
}
// Failed by MSVC
static_assert(sizeof(ST3) == 2);
// Failed by MSVC
static_assert(offsetof(ST3,s2) != offsetof(ST3,s1.s1), "Subobjects of same type must not have same pointer");
size_t size_st3() {
return sizeof(ST3);
}
size_t offset_st3() {
return offsetof(ST3, s2);
}
static_assert(sizeof(ST4) == 1);
static_assert(offsetof(ST4,s2) == offsetof(ST4,s1.s1), "Subobjects of different types where at least one is zero-sized may have same pointer");
size_t size_st4() {
return sizeof(ST4);
}
size_t offset_st4() {
return offsetof(ST4, s2);
}
Example output: https://godbolt.org/z/xnrT5Tsq1
Relevant part of the spec:
Unless an object is a bit-field or a subobject of zero size, the address of that object is the address of the first
byte it occupies. Two objects with overlapping lifetimes that are not bit-fields may have the same address if
one is nested within the other, or if at least one is a subobject of zero size and they are of different types;
otherwise, they have distinct addresses and occupy disjoint bytes of storage.
For ST4
the behavior of Clang and MSVC matches in all aspects, both compilers optimize to the expected extent.
For ST1
the layout does match, but actually not for the same reasons. MSVC preserves ST1::s1
as a unique address because of the unique sibling ST1::s2
. Clang preserves ST1::s1
because otherwise ST1::s2
would alias ST1::s1::s1
.
This difference then causes trouble in ST2
. Clang applies the optimization as strict aliasing rules render it non-ambiguous, but MSVC doesn't.
In ST3
we can see the opposite behavior. Even though everything is flagged with [[msvc::no_unique_address]]
, Clang still tries to avoid aliasing of ST1::s2
and ST1::s1::s1
. MSVC does not.
For comparison, GCC and ICC both match the behavior of Clang for all 4 examples.
See also microsoft/STL#1364 for the epic depending on ABI equality.
See also https://developercommunity.visualstudio.com/t/msvc::no_unique_address-fails-to-opt/10892814 for the corresponding defect report for MSVC for the cases ST2
and ST3
.