c++structc++20memory-layoutobject-identity

[[no_unique_address]] and two member values of the same type


I'm playing around with [[no_unique_address]] in c++20.

In the example on cppreference we have an empty type Empty and type Z

struct Empty {}; // empty class

struct Z {
    char c;
    [[no_unique_address]] Empty e1, e2;
};

Apparently, the size of Z has to be at least 2 because types of e1 and e2 are the same.

However, I really want to have Z with size 1. This got me thinking, what about wrapping Empty in some wrapper class with extra template parameter that enforces different types of e1 and e2.

template <typename T, int i>
struct Wrapper : public T{};

struct Z1 {
    char c;
    [[no_unique_address]] Wrapper<Empty,1> e1;
    [[no_unique_address]] Wrapper<Empty,2> e2;
};

Unfortunately, sizeof(Z1)==2. Is there a trick to make size of Z1 to be one?

I'm testing this with gcc version 9.2.1 and clang version 9.0.0


In my application, I have lots of empty types of the form

template <typename T, typename S>
struct Empty{
    [[no_unique_address]] T t;
    [[no_unique_address]] S s;
};

Which is an empty type if T and S are also empty types and distinct! I want this type to be empty even if T and S are the same types.


Solution

  • Which is an empty type if T and S are also empty types and distinct! I want this type to be empty even if T and S are the same types.

    You can't get that. Technically speaking, you can't even guarantee that it will be empty even if T and S are different empty types. Remember: no_unique_address is an attribute; the ability of it to hide objects is entirely implementation-dependent. From a standards perspective, you cannot enforce the size of empty objects.

    As C++20 implementations mature, you should assume that [[no_unique_address]] will generally follow the rules of empty base optimization. Namely, so long as two objects of the same type aren't subobjects, you can probably expect to get hiding. But at this point, it's kind of pot-luck.

    As to the specific case of T and S being the same type, that is simply not possible. Despite the implications of the name "no_unique_address", the reality is that C++ requires that, given two pointers to objects of the same type, those pointers either point to the same object or have different addresses. I call this the "unique identity rule", and no_unique_address does not affect that. From [intro.object]/9:

    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.

    Members of empty types declared as [[no_unique_address]] are zero-sized, but having the same type makes this impossible.

    Indeed, thinking about it, attempting to hide the empty type via nesting still violates the unique identity rule. Consider your Wrapper and Z1 case. Given a z1 which is an instance of Z1, it is clear that z1.e1 and z1.e2 are different objects with different types. However, z1.e1 is not nested within z1.e2 nor vice-versa. And while they have different types, (Empty&)z1.e1 and (Empty&)z1.e2 are not different types. But they do point to different objects.

    And by the unique identity rule, they must have different addresses. So even though e1 and e2 are nominally different types, their internals must also obey unique identity against other subobjects in the same containing object. Recursively.

    What you want is simply impossible in C++ as it currently stands, regardless of how you try.