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.
Which is an empty type if
T
andS
are also empty types and distinct! I want this type to be empty even ifT
andS
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; };
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With