【问题标题】:[[no_unique_address]] and two member values of the same type[[no_unique_address]] 和两个相同类型的成员值
【发布时间】:2020-03-25 07:21:09
【问题描述】:

我在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 的类型相同。

但是,我真的很想拥有大小为1Z。这让我开始思考,如何将 Empty 包装在一些带有额外模板参数的包装类中,该模板参数强制执行不同类型的 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 的大小变成一个?

我正在使用 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 是相同类型,我也希望此类型为空。

【问题讨论】:

  • T 本身添加模板参数怎么样?这将产生不同的类型。现在,Wrappers 都继承自 T 的事实阻碍了你......
  • @MaxLanghof 向T 添加模板参数是什么意思?现在,T 是一个模板参数。
  • 不要从T继承。
  • @Evg 在这里没有区别。
  • 仅仅因为它大于 1 并不能使它非空:coliru.stacked-crooked.com/a/51aa2be4aff4842e

标签: c++ struct c++20 memory-layout object-identity


【解决方案1】:

如果TS 也是空类型并且是不同的,那么哪个是空类型!即使TS 是相同类型,我也希望此类型为空。

你无法理解。从技术上讲,即使TS 是不同的空类型,你甚至不能保证它是空的。记住:no_unique_address 是一个属性;它隐藏对象的能力完全依赖于实现。从标准的角度来看,您不能强制规定空对象的大小。

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

至于TS是同一类型的具体情况,那根本不可能。尽管名称“no_unique_address”有含义,但实际情况是 C++ 要求,给定两个指向相同类型对象的指针,这些指针要么指向同一个对象,要么具有不同的地址。我将此称为“唯一身份规则”,no_unique_address 不会影响这一点。来自[intro.object]/9

如果一个对象嵌套在另一个对象中,或者如果至少一个对象是大小为零的子对象并且它们属于不同类型,则两个不是位域且具有重叠生命周期的对象可能具有相同的地址;否则,它们具有不同的地址并占用不相交的存储字节。

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

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

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

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

【讨论】:

  • no_unique_address 太令人失望了?。这是人们想要的东西(原始海报和我自己)的99%,然后在最后一刻通过说如果他们是同一类型,它忽略了你的属性并给他们唯一的地址。 ?‍♀️
  • @DwayneRobinson:这是人们需要的 100%。我认为这个问题中“想要”的东西是合理的。我的意思是,如果一个对象实际上是一个空类型,并且它不应该有一个唯一的地址......为什么你会有其中两个作为成员彼此相邻?该类型的每个实例都应该以相同的方式工作,对吗?那么为什么你有两个呢?
  • @DwayneRobinson:如果类型是模板,因此不知道给定的两种类型是相同的,那么更更重要不要违反唯一身份规则。你永远不知道什么时候某个未知类型可能会做一些事情,比如在某处存储对其类实例的引用或类似的疯狂事情。也就是说,它实际上可能会有意义地利用其独特的身份,而你不能只是让某个类否认它的这种能力。
【解决方案2】:

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

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

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

不幸的是 sizeof(Empty&lt;Empty&lt;A,A&gt;,A&gt;{})==2 其中 A 是一个完全空的结构。

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

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&lt;Empty&lt;A, char&gt;, A&gt; 之类的东西。

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

【讨论】:

  • 这很好,但不幸的是sizeof(Empty&lt;Empty&lt;A,A&gt;,A&gt;{})==2 其中A 是一个完全空的结构。
  • 我会添加一个get_empty&lt;T&gt; 函数。然后你可以重复使用左边或右边的get_empty&lt;T&gt;,如果它已经在那里工作的话。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-18
  • 1970-01-01
  • 1970-01-01
  • 2017-05-17
  • 2020-03-31
  • 2011-03-09
相关资源
最近更新 更多