重複する空のベースのストレージがvtableポインターと重複しないのはなぜですか?


11

この例を考えてみましょう:

#include <iostream>

int main()
{
    struct A {};
    struct B : A {};
    struct C : A, B {};

    std::cout << sizeof(A) << '\n'; // 1
    std::cout << sizeof(B) << '\n'; // 1
    std::cout << sizeof(C) << '\n'; // 2, because of a duplicate base

    struct E : A {virtual ~E() {}};
    struct F : A, B {virtual ~F() {}};

    std::cout << sizeof(E) << '\n'; // 8, the base overlaps the vtable pointer
    std::cout << sizeof(F) << '\n'; // 16, but why?
}

(ゴッドボルトで実行)

ここではstruct E、空の基本クラス(1バイトの大きさ)が、予想どおりvtableポインターと同じストレージを使用していることがわかります。

しかしstruct F、重複する空のベースを持つの場合、これは起こりません。何が原因ですか?

GCC、Clang、MSVCでも同じ結果が得られます。上記の結果はそう、x64のためのものですsizeof(void *) == 8


興味深いことに、struct G : A, B {void *ptr;};GCCとClangはEBO(サイズは8)を実行しますが、MSVCは実行しません(サイズは16)。


3
奇妙なことに、CA、から継承する)から継承することで、BフォームAB直接継承する場合とは異なる結果が得られます
Guillaume Racicot

1
私はこれを研究するのを楽しんだ。質問とリンクをありがとう。答えがあるかどうかわからないので、コメントします。これはCand の導出から導入されたあいまいさから生じるのFでしょうか?結局のところ、2 * sizeof(void*) == 16あなたが言ったようにx86_64で。(Story Tellerが言ったように)コンパイラーは完全に最適化することはできません。
Andrew Falanga

2
gccとclangはどちらもitanium ABIに準拠しているため、同じ結果が得られるのは正常です。これが当てはまる場合、ABIを定義するときに、レイアウトアルゴリズムが高すぎる可能性があることを恐れていたため、いくつかのショートカット(悲観化)を採用しました。
Marc Glisse、

2
@RianQuinnベースが重複しても構造が無効になることはありません。
HolyBlackCat

1
同じクラスから異なる「パス」を介して複数回継承する@RianQuinnは、C ++では完全に有効です。ひし形構造を作成する場合、つまり基本クラスを1回だけ持つ場合は、仮想継承を使用する必要があります。しかし、ひし形が不要で、基本クラスの重複が問題にならない場合は、これも言語の問題ではありません。OPのコードはA、を介して継承された2番目のにはBアクセスできないことを警告するだけです。それは結構です。例のように実際にアクセスしようとした場合にのみ、エラーが発生します。
sebrockm

回答:


4

コンパイラはstruct Aの後に1バイトのパディングを追加するため

F {vptr(8)+ Aからの0メンバー+ 1パディング(Aは空のため)+0からb} = 9の場合、コンパイラーは7バイトのパディングを追加して、構造体のストレージを調整します。

E {vptr(8)+ Aの0メンバー} = 8パディングは不要

マイクロソフトから

すべてのデータオブジェクトには、alignment-requirementがあります。構造の場合、要件はそのメンバーの中で最大です。すべてのオブジェクトにオフセットが割り当てられるため、オフセット%alignment-requirement == 0

https://docs.microsoft.com/en-us/cpp/c-language/storage-and-alignment-of-structures?view=vs-2019

編集:

ここに私のデモがあります:

int main()
{
    C c;
    A* a = &c;
    B* b = &c;

    std::cout << sizeof(A) << " " << a << '\n'; 
    std::cout << sizeof(B) << " " << b << '\n'; 
    std::cout << sizeof(C) << " " << &c << '\n'; 

    E e;
    a = &e;
    std::cout << sizeof(E) <<" " << &e << " " << a << '\n'; 

    F f;
    a = &f;
    b = &f;
    std::cout << sizeof(F) << " " << &f << " " << a << " " << b << '\n';

}

出力:

1 0000007A45B7FBB4
1 0000007A45B7FBB5
1 0000007A45B7FBB4
8 0000007A45B7FC18 0000007A45B7FC20
16 0000007A45B7FC38 0000007A45B7FC40 0000007A45B7FC41

あなたが見ることができるので、aとbはお互いに決してオーバーラップせず、多重継承のvptrではそれぞれに独自のポインタ値があります

VC2019 x64ビルドでコンパイルされたメモ


私はこれがどのように機能するかを考えていません。Aメンバーがいなくても、1バイトを占有します(他のオブジェクトと共有できます)。ではEAvptrの後にはありません。vptrの最初のバイトと重複しています。(ここにデモがありAます。アクセスできるようにコードを少し変更しました。)同じことが最初のAでも起こりFます。以来A(とがB)vptrの上に配置することができ、それがために発生しません。なぜ、私はわかりませんB
HolyBlackCat

@HolyBlackCatしかし、これはチェックテストコードで起こったことです
Ahmed

ああ、MSVCはGCC / Clangとは異なる動作をします。Avptrの上に置くほどスマートではありません。MSVCで出力が16になる理由を説明できますが、GCCとClangで何が起こっているのかわかりません。
HolyBlackCat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.