Standard-layout and tail padding

霸气de小男生 提交于 2019-11-30 08:10:14

The answer to this question doesn't come from the standard but rather from the Itanium ABI (which is why gcc and clang have one behavior but msvc does something else). That ABI defines a layout, the relevant parts of which for the purposes of this question are:

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.

and

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.

Where the placement of members other than virtual base classes is defined as:

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 ...].

The term POD has disappeared from the C++ standard, but it means standard-layout and trivially copyable. In this question, FooBeforeBase is a POD. The Itanium ABI ignores tail padding - hence dsize(FooBeforeBase) is 16.

But FooAfterBase is not a POD (it is trivially copyable, but it is not standard-layout). As a result, tail padding is not ignored, so dsize(FooAfterBase) is just 12, and the float can go right there.

This has interesting consequences, as pointed out by Quuxplusone in a related answer, implementors also typically assume that tail padding isn't reused, which wreaks havoc on this example:

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

Here, = does the right thing (it does not override B's tail padding), but copy() has a library optimization that reduces to memmove() - which does not care about tail padding because it assumes it does not exist.

FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));

If the additional data member was placed in the hole, memcpy would have overwritten it.

As is correctly pointed out in comments, the standard doesn't require that this memcpy invocation should work. However the Itanium ABI is seemingly designed with this case in mind. Perhaps the ABI rules are specified this way in order to make mixed-language programming a bit more robust, or to preserve some kind of backwards compatibility.

Relevant ABI rules can be found here.

A related answer can be found here (this question might be a duplicate of that one).

Here is a concrete case which demonstrates why the second case cannot reuse the padding:

union bob {
  FooBeforeBase a;
  FooBefore b;
};

bob.b.value = 3.14;
memset( &bob.a, 0, sizeof(bob.a) );

this cannot clear bob.b.value.

union bob2 {
  FooAfterBase a;
  FooAfter b;
};

bob2.b.value = 3.14;
memset( &bob2.a, 0, sizeof(bob2.a) );

this is undefined behavior.

FooBefore is not std-layout either; two classes are declaring none-static data members(FooBefore and FooBeforeBase). Thus the compiler is allowed to arbitrarily place some data members. Hence the differences on different tool-chains arise. In a std-layout hierarchy, atmost one class(either the most derived class or at most one intermediate class) shall declare none-static data members.

Here's a similar case as n.m.'s answer.

First, let's have a function, which clears a FooBeforeBase:

void clearBase(FooBeforeBase *f) {
    memset(f, 0, sizeof(*f));
}

This is fine, as clearBase gets a pointer to FooBeforeBase, it thinks that as FooBeforeBase has standard-layout, so memsetting it is safe.

Now, if you do this:

FooBefore b;
b.value = 42;
clearBase(&b);

You don't expect, that clearBase will clear b.value, as b.value is not part of FooBeforeBase. But, if FooBefore::value was put into tail-padding of FooBeforeBase, it would been cleared as well.

Is there a required layout per the standard and, if not, why do gcc and clang do what they do?

No, tail-padding is not required. It is an optimization, which gcc and clang do.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!