web-dev-qa-db-ja.com

標準レイアウトとテールパディング

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);
_

レイアウトを調べることができます godboltのclangで サイズが変更された理由は、FooBeforeで、メンバーvalueがオフセット16に配置されていることです( FooBeforeBaseから8の完全な配置)に対して、FooAfterでは、メンバーvalueはオフセット12に配置されます(事実上、FooAfterBaseのテールパディングを使用)。

FooBeforeBaseが標準レイアウトであることは明らかですが、FooAfterBaseはそうではありません(非静的データメンバーがすべて同じアクセス制御を持っているわけではないため、 [class。 prop]/ )。しかし、FooBeforeBaseが標準レイアウトであるために、このパディングバイトの尊重が必要なのはどういうことでしょうか。

GccとclangはどちらもFooAfterBaseのパディングを再利用し、最終的にsizeof(FooAfter) == 16になります。しかし、MSVCはそうではなく、24になります。標準に従って必要なレイアウトがありますか?そうでない場合、なぜgccとclangはそれらが行うことを行うのですか?


いくつかの混乱があるので、片付けるだけです:

  • FooBeforeBaseは標準レイアウトです
  • FooBefore isnotE in-と同様に、それと基本クラスの両方に非静的データメンバーがあります この例
  • FooAfterBase isnot(異なるアクセスの非静的データメンバーがあります)
  • FooAfter isnot(上記の両方の理由により)
20
Barry

この質問への答えは、標準からではなく、Itanium ABIから来ています(これが、gccとclangの動作は1つですが、msvcは別の動作をする理由です)。そのABIは レイアウト を定義します。この質問の目的に関連する部分は次のとおりです。

仕様内部の目的で、以下も指定します。

  • dsize(O):オブジェクトのデータサイズ、つまりテールパディングなしのOのサイズ。

そして

PODのテールパディングは無視します。これは、標準の初期バージョンでは他の用途に使用できず、タイプのコピーが高速になる場合があるためです。

仮想基本クラス以外のメンバーの配置が次のように定義されている場合:

オフセットdsize(C)から開始し、基本クラスの場合はnvalign(D)に、データメンバーの場合はalign(D)に揃えるために、必要に応じてインクリメントします。 [...関連性がない...]でない限り、Dをこのオフセットに配置します。

PODという用語はC++標準から消えましたが、標準レイアウトであり、簡単にコピーできることを意味します。この質問では、FooBeforeBaseはPODです。 Itanium ABIはテールパディングを無視します-したがって、dsize(FooBeforeBase)は16です。

ただし、FooAfterBaseはPODではありません(簡単にコピーできますが、not標準レイアウトです)。その結果、テールパディングは無視されないため、dsize(FooAfterBase)はわずか12であり、floatはすぐそこに移動できます。

これは興味深い結果をもたらします。Quuxplusoneが 関連する回答 で指摘しているように、実装者は通常、テールパディングが再利用されていないと想定しているため、この例では大混乱になります。

_#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()に還元されるライブラリ最適化があります。テールパディングは存在しないと想定しているため、テールパディングは気にしません。

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

追加のデータメンバーが穴に配置された場合、memcpyはそれを上書きします。

コメントで正しく指摘されているように、標準では、このmemcpy呼び出しが機能する必要はありません。ただし、Itanium ABIは、このケースを念頭に置いて設計されているようです。おそらく、混合言語プログラミングをもう少し堅牢にするため、またはある種の下位互換性を維持するために、ABIルールがこのように指定されています。

関連するABIルールを見つけることができます ここ

関連する答えを見つけることができます ここ (この質問はその質問と重複している可能性があります)。

1
n.m.