标准布局和尾部填充
Standard-layout and tail padding
David Hollman 最近在推特上发布了以下示例(我略微做了缩减):
struct FooBeforeBase {
double d;
bool b[4];
};
struct FooBefore : FooBeforeBase {
float value;
};
static_assert(sizeof(FooBefore) > 16);
//----------------------------------------------------
struct FooAfterBase {
protected:
double d;
public:
bool b[4];
};
struct FooAfter : FooAfterBase {
float value;
};
static_assert(sizeof(FooAfter) == 16);
您可以检查布局 in clang on godbolt 并发现大小改变的原因是在 FooBefore
中,成员 value
位于偏移量 16(保持与8 来自 FooBeforeBase
),而在 FooAfter
中,成员 value
位于偏移量 12(有效使用 FooAfterBase
的尾部填充)。
我很清楚 FooBeforeBase
是标准布局,但 FooAfterBase
不是(因为它的非静态数据成员并不都具有相同的访问控制,[class.prop]/3).但是,FooBeforeBase
作为标准布局需要这种填充字节的原因是什么?
gcc 和 clang 都重用 FooAfterBase
的填充,以 sizeof(FooAfter) == 16
结尾。但是 MSVC 没有,最后是 24。是否有每个标准所需的布局,如果没有,为什么 gcc 和 clang 会做他们做的事情?
有些混乱,所以只是为了澄清:
FooBeforeBase
是标准布局
FooBefore
是 而不是 (它和基 class 都有非静态数据成员,类似于 E
中的 this example)
FooAfterBase
是 而不是 (它具有不同访问权限的非静态数据成员)
FooAfter
是 而不是 (出于上述两个原因)
这是一个具体的案例,它说明了为什么第二种情况不能重用填充:
union bob {
FooBeforeBase a;
FooBefore b;
};
bob.b.value = 3.14;
memset( &bob.a, 0, sizeof(bob.a) );
这无法清除 bob.b.value
。
union bob2 {
FooAfterBase a;
FooAfter b;
};
bob2.b.value = 3.14;
memset( &bob2.a, 0, sizeof(bob2.a) );
这是未定义的行为。
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));
如果额外的数据成员被放置在空洞中,memcpy
会覆盖它。
正如评论中正确指出的那样,标准不要求此 memcpy
调用应该有效。然而,Itanium ABI 似乎是为这种情况而设计的。也许以这种方式指定 ABI 规则是为了使混合语言编程更加健壮,或者保持某种向后兼容性。
可以找到相关的ABI规则here。
可以找到相关答案here(这个问题可能与那个问题重复)。
这里有一个与 n.m. 的回答类似的案例。
首先,让我们有一个函数,它清除 FooBeforeBase
:
void clearBase(FooBeforeBase *f) {
memset(f, 0, sizeof(*f));
}
这很好,因为 clearBase
得到一个指向 FooBeforeBase
的指针,它认为 FooBeforeBase
有标准布局,所以 memsetting 它是安全的。
现在,如果你这样做:
FooBefore b;
b.value = 42;
clearBase(&b);
您不会期望 clearBase
会清除 b.value
,因为 b.value
不是 FooBeforeBase
的一部分。但是,如果 FooBefore::value
被放入 FooBeforeBase
的尾部填充,它也会被清除。
Is there a required layout per the standard and, if not, why do gcc and clang do what they do?
不,尾部填充不是必需的。这是 gcc 和 clang 做的优化。
FooBefore
也不是标准布局;两个 classes 正在声明 none-静态数据成员(FooBefore
和 FooBeforeBase
)。因此允许编译器任意放置一些数据成员。因此,出现了不同工具链的差异。
在标准布局层次结构中,至多一个 class(最派生的 class 或至多一个中间 class)应声明 none-static 数据成员。
这个问题的答案不是来自标准,而是来自 Itanium ABI(这就是为什么 gcc 和 clang 有一种行为,而 msvc 有其他行为)。该 ABI 定义了 a layout,与此问题相关的部分是:
For purposes internal to the specification, we also specify:
- dsize(O): the data size of an object, which is the size of O without tail padding.
和
We ignore tail padding for PODs because an early version of the standard did not allow us to use it for anything else and because it sometimes permits faster copying of the type.
除虚拟基地类以外的其他成员的安置定义为:
Start at offset dsize(C), incremented if necessary for alignment to nvalign(D) for base classes or to align(D) for data members. Place D at this offset unless [... not relevant ...].
术语 POD 已从 C++ 标准中消失,但它表示标准布局和简单可复制。在这个问题中,FooBeforeBase
是一个 POD。 Itanium ABI 忽略尾部填充 - 因此 dsize(FooBeforeBase)
是 16.
但是 FooAfterBase
不是 POD(它可以简单地复制,但它 不是 标准布局)。结果,尾部填充没有被忽略,所以 dsize(FooAfterBase)
只是 12,而 float
可以就在那里。
正如 Quuxplusone 在 related answer 中所指出的那样,这会产生有趣的后果,实施者通常还假设不重复使用尾部填充,这对这个示例造成了严重破坏:
#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
}
在这里,=
做了正确的事情(它没有覆盖 B
的尾部填充),但是 copy()
有一个库优化减少到 memmove()
- 它不关心尾部填充,因为它假定它不存在。
David Hollman 最近在推特上发布了以下示例(我略微做了缩减):
struct FooBeforeBase {
double d;
bool b[4];
};
struct FooBefore : FooBeforeBase {
float value;
};
static_assert(sizeof(FooBefore) > 16);
//----------------------------------------------------
struct FooAfterBase {
protected:
double d;
public:
bool b[4];
};
struct FooAfter : FooAfterBase {
float value;
};
static_assert(sizeof(FooAfter) == 16);
您可以检查布局 in clang on godbolt 并发现大小改变的原因是在 FooBefore
中,成员 value
位于偏移量 16(保持与8 来自 FooBeforeBase
),而在 FooAfter
中,成员 value
位于偏移量 12(有效使用 FooAfterBase
的尾部填充)。
我很清楚 FooBeforeBase
是标准布局,但 FooAfterBase
不是(因为它的非静态数据成员并不都具有相同的访问控制,[class.prop]/3).但是,FooBeforeBase
作为标准布局需要这种填充字节的原因是什么?
gcc 和 clang 都重用 FooAfterBase
的填充,以 sizeof(FooAfter) == 16
结尾。但是 MSVC 没有,最后是 24。是否有每个标准所需的布局,如果没有,为什么 gcc 和 clang 会做他们做的事情?
有些混乱,所以只是为了澄清:
FooBeforeBase
是标准布局FooBefore
是 而不是 (它和基 class 都有非静态数据成员,类似于E
中的 this example)FooAfterBase
是 而不是 (它具有不同访问权限的非静态数据成员)FooAfter
是 而不是 (出于上述两个原因)
这是一个具体的案例,它说明了为什么第二种情况不能重用填充:
union bob {
FooBeforeBase a;
FooBefore b;
};
bob.b.value = 3.14;
memset( &bob.a, 0, sizeof(bob.a) );
这无法清除 bob.b.value
。
union bob2 {
FooAfterBase a;
FooAfter b;
};
bob2.b.value = 3.14;
memset( &bob2.a, 0, sizeof(bob2.a) );
这是未定义的行为。
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));
如果额外的数据成员被放置在空洞中,memcpy
会覆盖它。
正如评论中正确指出的那样,标准不要求此 memcpy
调用应该有效。然而,Itanium ABI 似乎是为这种情况而设计的。也许以这种方式指定 ABI 规则是为了使混合语言编程更加健壮,或者保持某种向后兼容性。
可以找到相关的ABI规则here。
可以找到相关答案here(这个问题可能与那个问题重复)。
这里有一个与 n.m. 的回答类似的案例。
首先,让我们有一个函数,它清除 FooBeforeBase
:
void clearBase(FooBeforeBase *f) {
memset(f, 0, sizeof(*f));
}
这很好,因为 clearBase
得到一个指向 FooBeforeBase
的指针,它认为 FooBeforeBase
有标准布局,所以 memsetting 它是安全的。
现在,如果你这样做:
FooBefore b;
b.value = 42;
clearBase(&b);
您不会期望 clearBase
会清除 b.value
,因为 b.value
不是 FooBeforeBase
的一部分。但是,如果 FooBefore::value
被放入 FooBeforeBase
的尾部填充,它也会被清除。
Is there a required layout per the standard and, if not, why do gcc and clang do what they do?
不,尾部填充不是必需的。这是 gcc 和 clang 做的优化。
FooBefore
也不是标准布局;两个 classes 正在声明 none-静态数据成员(FooBefore
和 FooBeforeBase
)。因此允许编译器任意放置一些数据成员。因此,出现了不同工具链的差异。
在标准布局层次结构中,至多一个 class(最派生的 class 或至多一个中间 class)应声明 none-static 数据成员。
这个问题的答案不是来自标准,而是来自 Itanium ABI(这就是为什么 gcc 和 clang 有一种行为,而 msvc 有其他行为)。该 ABI 定义了 a layout,与此问题相关的部分是:
For purposes internal to the specification, we also specify:
- dsize(O): the data size of an object, which is the size of O without tail padding.
和
We ignore tail padding for PODs because an early version of the standard did not allow us to use it for anything else and because it sometimes permits faster copying of the type.
除虚拟基地类以外的其他成员的安置定义为:
Start at offset dsize(C), incremented if necessary for alignment to nvalign(D) for base classes or to align(D) for data members. Place D at this offset unless [... not relevant ...].
术语 POD 已从 C++ 标准中消失,但它表示标准布局和简单可复制。在这个问题中,FooBeforeBase
是一个 POD。 Itanium ABI 忽略尾部填充 - 因此 dsize(FooBeforeBase)
是 16.
但是 FooAfterBase
不是 POD(它可以简单地复制,但它 不是 标准布局)。结果,尾部填充没有被忽略,所以 dsize(FooAfterBase)
只是 12,而 float
可以就在那里。
正如 Quuxplusone 在 related answer 中所指出的那样,这会产生有趣的后果,实施者通常还假设不重复使用尾部填充,这对这个示例造成了严重破坏:
#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 }
在这里,=
做了正确的事情(它没有覆盖 B
的尾部填充),但是 copy()
有一个库优化减少到 memmove()
- 它不关心尾部填充,因为它假定它不存在。