gcc/clang在基础结构的后填充中布局派生结构的字段

use*_*059 7 c++ gcc padding clang

当涉及填充和继承时,我对gcc和clang如何布局结构感到困惑.这是一个示例程序:

#include <string.h>
#include <stdio.h>

struct A
{
    void* m_a;
};

struct B: A
{
    void* m_b1;
    char m_b2;
};

struct B2
{
    void* m_a;
    void* m_b1;
    char m_b2;
};

struct C: B
{
    short m_c;
};

struct C2: B2
{
    short m_c;
};

int main ()
{
    C c;
    memset (&c, 0, sizeof (C));
    memset ((B*) &c, -1, sizeof (B));

    printf (
        "c.m_c = %d; sizeof (A) = %d sizeof (B) = %d sizeof (C) = %d\n", 
        c.m_c, sizeof (A), sizeof (B), sizeof (C)
        );

    C2 c2;
    memset (&c2, 0, sizeof (C2));
    memset ((B2*) &c2, -1, sizeof (B2));

    printf (
        "c2.m_c = %d; sizeof (A) = %d sizeof (B2) = %d sizeof (C2) = %d\n", 
        c2.m_c, sizeof (A), sizeof (B2), sizeof (C2)
        );

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

$ ./a.out
c.m_c = -1; sizeof (A) = 8 sizeof (B) = 24 sizeof (C) = 24
c2.m_c = 0; sizeof (A) = 8 sizeof (B2) = 24 sizeof (C2) = 32
Run Code Online (Sandbox Code Playgroud)

结构C1和C2的布局不同.在C1中,m_c被分配在struct B1的后填充中,因此被第二个memset()覆盖; 与C2不会发生.

使用的编译器:

$ clang --version
Ubuntu clang version 3.3-16ubuntu1 (branches/release_33) (based on LLVM 3.3)
Target: x86_64-pc-linux-gnu
Thread model: posix

$ c++ --version
c++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Run Code Online (Sandbox Code Playgroud)

使用-m32选项也是如此(显然输出中的大小会有所不同).

x86和x86_64版本的Microsoft Visual Studio 2010 C++编译器都没有此问题(即它们以相同的方式布局结构С1和C2)

如果它不是一个bug并且是设计的,那么我的问题是:

  1. 在后填充中分配或不分配派生结构的字段的精确规则是什么(例如为什么C2不会发生?)
  2. 是否有任何方法可以使用开关/属性覆盖此行为(即布局就像MSVC一样)?

提前致谢.

弗拉基米尔

Quu*_*one 5

对于每个对这个问题投反对票的人,以及 OP 对他的手写体有多糟糕的自以为是的愤慨的自我回答memcpy......考虑到 libc++ 和 libstdc++ 的实现者陷入了完全相同的坑。在可预见的未来,了解何时重用尾部填充以及何时不重用实际上非常重要。在 OP 上很好地提出了这个问题。

结构布局的 Itanium ABI 规则在这里。相关的措辞是

如果 D 是基类,则将 sizeof(C) 更新为 max (sizeof(C), offset(D)+nvsize(D))。

这里“[POD 类型]的 dsize、nvsize 和 nvalign 被定义为它们的普通大小和对齐方式”,但非 POD 类型的 nvsize 被定义为“对象的非虚拟大小,即O 的大小没有虚拟碱基[也没有尾部填充]。” 所以如果 D 是 POD,我们永远不会在它的尾部填充中嵌入任何东西;而如果 D不是POD,我们就可以将下一个成员(或基数)嵌套到它的尾部填充中。

因此,任何非 POD 类型(即使是可简单复制的类型!)都必须考虑将重要数据填充到尾部填充中的可能性。这通常违反了实现者关于允许对可简单复制的类型做什么的假设(即,您可以简单地复制它们)。

Wandbox 测试用例:

#include <algorithm>
#include <stdio.h>

struct A {
    int m_a;
};

struct B : A {
    int m_b1;
    char m_b2;
};

struct C : B {
    short m_c;
};

int main() {
    C c1 { 1, 2, 3, 4 };
    B& b1 = c1;
    B b2 { 5, 6, 7 };

    printf("before operator=: %d\n", int(c1.m_c));  // 4
    b1 = b2;
    printf("after operator=: %d\n", int(c1.m_c));  // 4

    printf("before std::copy: %d\n", int(c1.m_c));  // 4
    std::copy(&b2, &b2 + 1, &b1);
    printf("after std::copy: %d\n", int(c1.m_c));  // 64, or 0, or anything but 4
}
Run Code Online (Sandbox Code Playgroud)


Pup*_*ppy 1

您的代码表现出未定义的行为,因为 C 和 C2 不是 POD,并且不允许对其数据的随机位进行内存复制。

然而,从长远来看,这是一个复杂的问题。平台 (Unix) 上现有的 C ABI 允许这种行为(这是针对 C++98 的,它允许这种行为)。然后委员会改变了C++03和C++11中不兼容的规则。Clang 至少可以切换到更新的规则。当然,Unix 上的 C ABI 并没有改变以适应将内容放入填充的新 C++11 规则,因此编译器不能完全更新,因为这会破坏所有 ABI。

我相信 GCC 正在为 5.0 存储破坏 ABI 的更改,这可能就是其中之一。

Windows 总是在其 C ABI 中禁止这种做法,因此据我所知,没有问题。