【问题标题】:What prevents overlapping of adjacent members in classes?什么可以防止类中相邻成员的重叠?
【发布时间】:2020-05-09 01:58:24
【问题描述】:

考虑以下三个structs:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

int为4字节的典型平台上,大小、对齐方式和总填充1如下:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

blubblob 成员的存储之间没有重叠,即使大小 1 blob 原则上可以“适合”blub 的填充。

C++20 引入了no_unique_address 属性,允许相邻的空成员共享同一个地址。它还明确允许上述使用一个成员的填充来存储另一个成员的场景。来自cppreference(强调我的):

表示该数据成员不需要具有与其类的所有其他非静态数据成员不同的地址。这意味着如果成员有一个空类型(例如无状态分配器),编译器可以优化它以不占用空间,就像它是一个空基一样。 如果成员不为空,则其中的任何尾部填充也可以重复用于存储其他数据成员。

确实,如果我们在blub b0上使用这个属性,bla的大小会下降到8,所以blob确实存储在blubas seen on godbolt中。

最后,我们来回答我的问题:

标准中的哪些文本(C++11 到 C++20)可以防止在没有 no_unique_address 的情况下出现这种重叠,对于不可复制的对象?

我需要从上面排除普通可复制 (TC) 对象,因为对于 TC 对象,允许 std::memcpy 从一个对象到另一个对象,包括成员子对象,如果存储重叠,这会中断(因为将覆盖相邻成员的全部或部分存储空间)2.


1我们将填充简单地计算为结构大小与其所有组成成员的大小之间的差值,递归。

2这就是我定义复制构造函数的原因:使blubblob 不能简单地复制

【问题讨论】:

  • 我没有研究过,但我猜测“好像”规则。如果与抽象机器(这是您的代码编译所针对的)没有可观察到的差异(一个具有非常特定含义的术语),那么编译器可以随意更改代码。
  • 很确定这是一个骗局:stackoverflow.com/questions/53837373/…
  • @JesperJuhl - 对,但我问的是为什么不能,而不是为什么可以,以及“好像”规则通常适用于前者,但对后者没有意义。此外,“好像”对于通常是全局问题而非局部问题的结构布局并不清楚。最终,编译器必须有一套一致的布局规则,除了可能证明永远不会“转义”的结构。
  • @BeeOnRope 我无法回答您的问题,抱歉。这就是为什么我只发表评论而不是答案的原因。您在该评论中得到的答案是我对解释的最佳猜测,但我不知道答案(我很想自己学习 - 这就是您获得支持的原因)。
  • @NicolBolas - 您是否回答了正确的问题?这与检测安全副本或其他任何内容无关。相反,我很好奇为什么不能在成员之间重复使用填充。无论如何,你都错了:trivially copyableproperty of the type,而且一直都是。然而,为了安全地复制一个对象,它必须 both 具有 TC 类型(类型的属性),而不是潜在重叠的主体(对象的属性,我猜这是你弄糊涂了)。仍然不知道为什么我们在这里谈论副本。

标签: c++ language-lawyer c++20


【解决方案1】:

在谈论内存模型时,该标准非常安静,并且对它使用的某些术语也不是很明确。但我想我找到了一个可行的论点(可能有点弱)

首先,让我们找出什么是对象的一部分。 [basic.types]/4:

T 类型的对象的对象表示是Nunsigned char 对象的序列被T 类型的对象占用,其中N 等于sizeof(T)T 类型对象的值表示是参与表示T 类型值的一组位。对象表示中不属于值表示的位是填充位。

所以b0的对象表示由sizeof(blub)unsigned char对象组成,所以8个字节。填充位是对象的一部分。

如果不是嵌套在其中的对象,则任何对象都不能占用另一个对象的空间[basic.life]/1.5

T 类型的对象 o 的生命周期在以下时间结束:

[...]

(1.5) 对象占用的存储空间被释放,或者被未嵌套在o ([intro.object]) 中的对象重用。

所以b0 的生命周期将结束,当它占用的存储空间将被另一个对象重用时,即b1。我还没有检查过,但我认为标准要求活动对象的子对象也应该是活动的(我无法想象这应该如何以不同的方式工作)。

所以b0 占用的存储空间可能不会被b1 使用。我在标准中没有找到“占用”的定义,但我认为合理的解释是“对象表示的一部分”。在描述对象表示的引用中,使用了单词“take up”1。在这里,这将是 8 个字节,因此 bla 至少需要一个以上的 b1

特别是对于子对象(因此在其他非静态数据成员中),还有[intro.object]/9 的规定(但这是在 C++20 中添加的,谢谢@BeeOnRope)

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

(强调我的)在这里,我们再次遇到“占用”未定义的问题,我再次主张在对象表示中获取字节。注意这个[basic.memobj]/footnote 29有一个脚注

根据“as-if”规则,允许实现将两个对象存储在同一机器地址,或者如果程序无法观察到差异([intro.execution])则根本不存储对象。

如果编译器可以证明没有可观察到的副作用,它可能会允许编译器打破这一点。我认为这对于像对象布局这样的基本事物来说是相当复杂的。也许这就是为什么只有当用户通过添加[no_unique_address] 属性来提供没有理由拥有不相交对象的信息时才进行这种优化。

tl;dr:填充可能是对象的一部分,并且成员必须是不相交的。


1 我忍不住添加了一个引用可能意味着占用:Webster’s Revised Unabridged Dictionary, G. & C. Merriam, 1913(强调我的)

  1. 保持或填充尺寸; 占用房间或空间;覆盖或填充;因为,营地占地五英亩。 J. Herschel 爵士。

如果没有字典爬取,什么样的标准爬取是完整的?

【讨论】:

  • into.storage 中的“占用不相交的存储字节”部分对我来说已经足够了,但这个措辞仅在 C++20 中添加,作为添加 @ 的更改的一部分987654348@。它使 C++20 之前的情况不太清楚。我不明白您从 basic.life/1.5 导致“如果对象不是嵌套在其中,则任何对象都不能占用另一个对象的空间”的推理,特别是如何从“对象占用的存储空间被释放”中获取到“任何物体都不能占据另一个物体的空间”。
  • 我在该段落中添加了一个小说明。我希望这使它更容易理解。否则我明天再看一遍,现在对我来说已经很晚了。
  • "如果一个对象嵌套在另一个对象中,或者如果至少一个是大小为零的子对象并且它们是不同类型” 2 objects with overlapping lifetimes, of the same type, have the same address.
  • 抱歉,您能详细说明一下吗?您从我的回答中引用了一个标准引用,并举了一个与之有点冲突的例子。我不确定这是否是对我的回答的评论,以及它是否应该告诉我。关于您的示例,我会说必须考虑标准的其他部分(有一段关于为另一个对象提供存储的无符号字符数组,关于零大小基础优化的内容,如果放置新的有特殊津贴,我认为与 OP 无关的所有事情示例)
  • @n314159 我认为这个措辞可能有缺陷。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-31
  • 1970-01-01
  • 2015-09-21
  • 1970-01-01
相关资源
最近更新 更多