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

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.


As far as I can tell, that is not possible if you want to have both members. But you can specialise and have only one of the members when the type is same and empty:

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

    constexpr T& get_t() noexcept { return t; };
    constexpr S& get_s() noexcept { return s; };
};

template<typename TS>
struct Empty<TS, TS, typename std::enable_if_t<std::is_empty_v<TS>>>{
    [[no_unique_address]] TS ts;

    constexpr TS& get_t() noexcept { return ts; };
    constexpr TS& get_s() noexcept { return ts; };
};

Of course, rest of the program that uses the the members would need to be changed to deal with the case where there is only one member. It shouldn't matter which member is used in this case - after all, it is a stateless object with no unique address. The shown member functions should make that simple.

unfortunately sizeof(Empty<Empty<A,A>,A>{})==2 where A is a completely empty struct.

You could introduce more specialisations to support recursive compression of empty pairs:

template<class TS>
struct Empty<Empty<TS, TS>, TS, typename std::enable_if_t<std::is_empty_v<TS>>>{
    [[no_unique_address]] Empty<TS, TS> ts;

    constexpr Empty<TS, TS>& get_t() noexcept { return ts; };
    constexpr TS&            get_s() noexcept { return ts.get_s(); };
};

template<class TS>
struct Empty<TS, Empty<TS, TS>, typename std::enable_if_t<std::is_empty_v<TS>>>{
    [[no_unique_address]] Empty<TS, TS> ts;

    constexpr TS&            get_t() noexcept { return ts.get_t(); };
    constexpr Empty<TS, TS>& get_s() noexcept { return ts; };
};

Even more, to compress something like Empty<Empty<A, char>, A>.

template <typename T, typename S>
struct Empty<Empty<T, S>, S, typename std::enable_if_t<std::is_empty_v<S>>>{
     [[no_unique_address]] Empty<T, S> ts;

    constexpr Empty<T, S>& get_t() noexcept { return ts; };
    constexpr S&           get_s() noexcept { return ts.get_s(); };
};

template <typename T, typename S>
struct Empty<Empty<S, T>, S, typename std::enable_if_t<std::is_empty_v<S>>>{
     [[no_unique_address]] Empty<S, T> st;

    constexpr Empty<S, T>& get_t() noexcept { return st; };
    constexpr S&           get_s() noexcept { return st.get_t(); };
};


template <typename T, typename S>
struct Empty<T, Empty<T, S>, typename std::enable_if_t<std::is_empty_v<T>>>{
     [[no_unique_address]] Empty<T, S> ts;

    constexpr T&           get_t() noexcept { return ts.get_t(); };
    constexpr Empty<T, S>  get_s() noexcept { return ts; };
};

template <typename T, typename S>
struct Empty<T, Empty<S, T>, typename std::enable_if_t<std::is_empty_v<T>>>{
     [[no_unique_address]] Empty<S, T> st;

    constexpr T&           get_t() noexcept { return st.get_s(); };
    constexpr Empty<S, T>  get_s() noexcept { return st; };
};