[[no_unique_address]] 和两个相同类型的成员值

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

我在 c++20.

中和 [[no_unique_address]]

cppreference 的示例中,我们有一个空类型 Empty 和类型 Z

struct Empty {}; // empty class

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

显然,Z 的大小必须至少为 2,因为 e1e2 的类型相同。

不过,我真的很想 Z 大小 1。这让我开始思考,如何将 Empty 包装在一些带有额外模板参数的包装器 class 中,强制执行不同类型的 e1e2.

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;
};

不幸的是,sizeof(Z1)==2。有什么技巧可以使 Z1 的大小为 1 吗?

我正在使用 gcc version 9.2.1clang version 9.0.0

进行测试

在我的应用程序中,我有很多表单的空类型

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

如果 TS 也是空类型并且不同,则这是空类型!我希望这种类型为空,即使 TS 是相同的类型。

据我所知,如果您想同时拥有这两个成员,那是不可能的。但是当类型相同且为空时,您可以专门化并且只有一个成员:

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; };
};

当然,需要更改使用成员的程序的其余部分以处理只有一个成员的情况。在这种情况下使用哪个成员并不重要——毕竟,它是一个没有唯一地址的无状态对象。显示的成员函数应该使这变得简单。

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

您可以引入更多的特化来支持空对的递归压缩:

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; };
};

甚至更多,压缩 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; };
};

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.

你听不懂。从技术上讲,即使 TS 是不同的空类型,你甚至不能保证它会是空的。记住:no_unique_address 是一个属性;它隐藏对象的能力完全 取决于实现。从标准的角度来看,您不能强制执行空对象的大小。

随着 C++20 实现的成熟,您应该假设 [[no_unique_address]] 通常会遵循空基优化的规则。也就是说,只要相同类型的两个对象不是子对象,您就可能希望隐藏起来。但在这一点上,有点运气。

至于TS是同一类型的具体情况,那是根本不可能的。尽管名称“no_unique_address”有含义,但实际情况是 C++ 要求,给定两个指向相同类型对象的指针,这些指针要么指向同一个对象,要么具有不同的地址。我称之为“唯一身份规则”,no_unique_address 不会影响它。来自 [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.

声明为 [[no_unique_address]] 的空类型成员的大小为零,但具有相同的类型使得这不可能。

确实,仔细想想,试图通过嵌套来隐藏空类型,还是违反了唯一标识规则。考虑您的 WrapperZ1 案例。给定 z1Z1 的一个实例,很明显 z1.e1z1.e2 是具有不同类型的不同对象。但是,z1.e1 没有嵌套在 z1.e2 中,反之亦然。虽然它们有不同的类型,但 (Empty&)z1.e1(Empty&)z1.e2 而不是 不同的类型。但它们确实指向不同的对象。

并且根据唯一身份规则,它们必须具有不同的地址。因此,即使 e1e2 名义上是不同的类型,它们的内部结构也必须遵守与同一包含对象中的其他子对象的唯一标识。递归地。

无论您如何尝试,您想要的在目前的 C++ 中都是不可能的。