The note in [dcl.attr.nouniqueaddr]
says:
[Note 1: The non-static data member can share the address of another non-static data member or that of a base class, and any padding that would normally be inserted at the end of the object can be reused as storage for other members. — end note]
(bold mine)
Since the only normative effect of no_unique_address
is making the object potentially-overlapping, which base classes automatically are, the same effects should apply to base classes.
This appears to contradict [intro.object]/10
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.
This seemingly permits overlap only if one of the objects is zero-size (is potentially-overlapping and empty).
And indeed, neither GCC, Clang, nor MSVC allow non-empty objects to overlap: run on gcc.godbolt.org
struct A
{
int x;
short y;
};
struct B
{
short b;
};
struct C : A, B {};
struct D
{
[[no_unique_address]] A a;
[[no_unique_address]] B b;
};
// Those assertions pass, but I expected the size to be `sizeof(int) * 2` instead.
static_assert(sizeof(C) == sizeof(int) * 3);
static_assert(sizeof(D) == sizeof(int) * 3);
There's also [basic.types.general]/2
that governs memcpy-ing trivially copyable types:
For any object (other than a potentially-overlapping subobject) of trivially copyable type
T
, ... the underlying bytes ... can be copied into an array ofchar
...
(bold mine)
If only empty objects can overlap, then it's unclear why it bans all potentially-overlapping objects and not only the empty ones.
What's going on here? Can non-empty potentially-overlapping objects actually overlap, and is there a way to make compilers do so without extensions?
Found the proposal that added [[no_unique_address]]
. The plot thickens:
Does this allow reuse of tail padding?
Tail padding reuse is permitted for base classes, so it's also permitted for members with the attribute.
And the "disjoint bytes of storage" part was added by this very proposal, and didn't exist before. A regression perhaps?
My reading of the standard (as explained in the question) was wrong, the overlap is in fact allowed, and can be demonstrated in practice. This was pointed out to me by @stablewespe in the TCCPP Discord.
I thought that [intro.object]/10
disallows the overlap by saying
occupy disjoint bytes of storage
With the implicit assumption that an object occupies sizeof(T)
bytes of storage. Turns out it's not the case, potentially-overlapping subobjects can occupy less storage than their sizeof
.
... The result of applying
sizeof
to a potentially-overlapping subobject is the size of the type, not the size of the subobject.58 ...58 The actual size of a potentially-overlapping subobject can be less than the result of applying
sizeof
to the subobject, due to virtual base classes and less strict padding requirements on potentially-overlapping subobjects.
So the overlap is allowed.
As for why I couldn't observe it in practice, it seems that the Itanium ABI additionally disallows the overlap for POD types for C compatibility.
Modifying the type that has the trailing padding to be non-POD, such as by adding a non-trivial constructor or destructor (A() {}
or ~A() {}
), or by adding default member initializers, makes the overlap possible:
struct A
{
int x;
short y;
A() {}
};
struct B
{
short b;
};
struct C : A, B {};
#ifdef _MSC_VER
#define no_unique_address msvc::no_unique_address
#endif
struct D
{
[[no_unique_address]] A a;
[[no_unique_address]] B b;
};
// Those assertions pass, meaning the overlap is now there.
static_assert(sizeof(C) == sizeof(int) * 2);
static_assert(sizeof(D) == sizeof(int) * 2);
Though this still doesn't work on MSVC, for unknown reasons. I assume they've just messed up and didn't implement this optimization.