当涉及填充和继承时,我对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;
}
输出:
$ ./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
结构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.
-m32选项也是如此(显然输出中的大小会有所不同)。
x86和x86_64版本的Microsoft Visual Studio 2010 C ++编译器都没有此问题(即它们以相同的方式布置结构С1和C2)
如果它不是一个bug并且是设计的,那么我的问题是:
提前致谢。
弗拉基米尔
答案 0 :(得分:4)
对于每个反对这个问题和OP的人来说,对于他的手写memcpy
到底是多么糟糕,UB都自以为是地愤慨...考虑到libc ++和libstdc ++的实现者都陷入了同样的困境。在可预见的将来,了解何时重新使用填充以及何时不重新使用填充实际上非常重要。在OP上很好地提出了这个问题。
用于结构布局的Itanium ABI规则为here。相关措辞是
如果D是基类,则将sizeof(C)更新为最大值(sizeof(C),offset(D)+ nvsize(D))。
此处“将[POD类型]的dsize,nvsize和nvalign定义为它们的普通大小和对齐方式,”但非POD类型的nvsize定义为“ 非虚拟大小” ,即没有虚拟碱基(也没有尾部填充)的O的大小。”因此,如果D为POD,则我们永远不会在其尾部填充中嵌套任何内容;而如果D是 not POD,则可以将下一个成员(或碱基)嵌套在其尾部填充中。
因此,任何非POD类型(甚至是可微复制的类型!)都必须考虑其尾部填充中填充了重要数据的可能性。通常,这违反了实现者的假设,即允许对琐碎可复制类型进行处理(即,您可以琐碎地复制它们)。
#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
}
答案 1 :(得分:1)
您的代码显示未定义的行为,因为C和C2不是POD,并且不允许通过其数据的随机位进行memcpying。
然而,在稍长的时间内,这是一个复杂的问题。平台上现有的C ABI(Unix)允许这种行为(这适用于允许它的C ++ 98)。然后委员会改变了C ++ 03和C ++ 11中不兼容的规则。至少,Clang可以改用更新的规则。当然,Unix上的C ABI没有改变以适应新的C ++ 11规则来放置填充,因此编译器不能完全更新,因为这会破坏所有ABI。
我认为GCC正在存储5.0的ABI破坏性更改,这可能就是其中之一。
Windows一直禁止在他们的C ABI中使用这种做法,因此我没有意识到这一点。
答案 2 :(得分:1)
不同之处在于,如果该对象已经是&#34;而不仅仅是数据&#34;那么允许编译器使用前一个对象的填充。并且不支持使用memcpy
操纵它。
B
结构不仅仅是数据,因为它是派生对象,因此可以使用它的松弛空间,因为如果你memcpy
- B
1}}你周围的实例已经违反了合同。
B2
只是一个结构,向后兼容性要求它的大小(包括松弛空间)只是允许你使用memcpy
代码的内存。
答案 3 :(得分:0)
感谢大家的帮助。
底线,C ++编译器允许在布局派生结构的字段时重用非POD结构的尾部填充。 GCC和clang都使用了这个权限,MSVC没有。 海湾合作委员会似乎有-Wabi警告标志,这应该有助于捕捉潜在ABI不相容的案例,但它没有产生上述样本的警告。
看起来防止这种情况发生的唯一方法是注入显式的尾部填充字段。