21

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.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
tom
  • 1,520
  • 1
  • 12
  • 26
  • 2
    What about adding template arguments to `T` itself? That would generate distinct types. Right now the fact that both `Wrapper`s inherit from `T` is holding you back... – Max Langhof Nov 29 '19 at 17:42
  • @MaxLanghof What do you mean by adding a template argument to `T`? Right now, `T` is a template argument. – tom Nov 29 '19 at 17:49
  • Don't inherit from `T`. – Evg Nov 29 '19 at 17:50
  • @Evg makes no difference here. – eerorika Nov 29 '19 at 17:50
  • 3
    Just because it is bigger than 1 doesn't make it non-empty: http://coliru.stacked-crooked.com/a/51aa2be4aff4842e – Deduplicator Nov 29 '19 at 17:53
  • @Evg With your new suggestion of no composition, you've lost all information about the type argument. It would be even simpler to have no member at all, if you don't need the type. – eerorika Nov 29 '19 at 17:53
  • @eerorika, got it. – Evg Nov 29 '19 at 17:54
  • @Evg I would like that `Wrapper1` still holds a value of `T`. In 90% use cases I have , the type `T` is empty, but from time to time it is not and its value might be important. – tom Nov 29 '19 at 17:55

2 Answers2

8

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • no_unique_address is so disappointing . It's 99% of the way there for what people want (the original poster and myself), and then drops the ball at the last moment by saying that *if* they're the same type, it ignores your attribute and gives them unique addresses anyway. ‍♀️ – Dwayne Robinson Mar 22 '21 at 02:02
  • @DwayneRobinson: It's 100% of the way there for what people *need*. I contest that what is "wanted" in this question is a reasonable thing to want. I mean, if an object is in fact an empty type, and it shouldn't have a unique address... why would you have *two of them* as members right next to each other? Every instance of that type ought to work the same, right? So why do you have two of them? – Nicol Bolas Mar 22 '21 at 02:06
  • @DwayneRobinson: And if the type is a template and therefore doesn't know that the two types it is given are the same, then it's even *more important* not to violate the unique identity rule. You never know when some unknown type might do something like store references to instances of its class somewhere or something crazy like that. That is, it might actually meaningfully take advantage of its unique identity, and you can't just have some class deny it that capability. – Nicol Bolas Mar 22 '21 at 02:09
2

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; };
};
eerorika
  • 232,697
  • 12
  • 197
  • 326