Dlaczego miejsce na zduplikowaną pustą bazę nie pokrywa się ze wskaźnikiem vtable?


11

Rozważ ten przykład:

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

(biegnij na godbolt)

Tutaj możesz to zobaczyć struct E pustej klasy podstawowej (która ma duży rozmiar 1 bajt), zgodnie z oczekiwaniami, używa tej samej pamięci co wskaźnik vtable.

Ale dla struct F , która ma zduplikowaną pustą bazę, tak się nie dzieje. Co to powoduje?

Ten sam wynik otrzymuję w GCC, Clang i MSVC. Powyższe wyniki dotyczą x64, więc sizeof(void *) == 8.


Co ciekawe, dla struct G : A, B {void *ptr;};GCC i Clanga wykonują EBO (rozmiar to 8), ale MSVC nie (rozmiar to 16).


3
O dziwo, dziedzicząc po C(dziedzicząc A, B) otrzymujesz inny wynik niż dziedziczenie formy Ai Bbezpośrednio
Guillaume Racicot

1
Podobało mi się badanie tego. Dzięki za pytanie i link. Nie jestem pewien, czy mam odpowiedź, więc po prostu skomentuję. Czy to możliwe, że wynika to z niejednoznaczności wynikającej z wyprowadzenia Ci F? W końcu 2 * sizeof(void*) == 16na x86_64, jak powiedziałeś. Kompilator nie może w pełni zoptymalizować (jak powiedział Story Teller), a więc nie.
Andrew Falanga,

2
To normalne, że otrzymujesz taki sam wynik na gcc i clang, ponieważ oba postępują zgodnie z itanium ABI. I jeśli tak jest, myślę, że podczas definiowania ABI bali się, że algorytm układu może stać się zbyt drogi, więc podjęli pewne skróty (inaczej pesymizacje).
Marc Glisse

2
@RianQuinn Zduplikowana baza nie powoduje, że struktura jest nieprawidłowa.
HolyBlackCat

1
@RianQuinn wielokrotnie dziedziczy po tej samej klasie przez różne „ścieżki” jest całkowicie poprawny w C ++. Jeśli chcesz utworzyć strukturę diamentową, tj. Mieć klasę bazową tylko raz, musisz użyć wirtualnego dziedziczenia. Ale jeśli nie chcesz diamentu i posiadanie zduplikowanej klasy podstawowej nie stanowi dla ciebie problemu, to nie jest to również problem dla języka. Kod OP generuje tylko ostrzeżenie, mówiąc, że nie można uzyskać dostępu do drugiego A, odziedziczonego przez B. W porządku. Tylko jeśli faktycznie spróbujesz uzyskać do niego dostęp, tak jak w twoim przykładzie, pojawi się błąd.
sebrockm

Odpowiedzi:


4

Ponieważ kompilator dodaje jeden bajt dopełniania po strukturze A

F {vptr (8) + 0 elementów z dopełnienia A + 1 (ponieważ A jest pusty) +0 z b} = 9, a następnie kompilator dodaje dopełnienie 7 bajtów, aby wyrównać pamięć struktury;

E {vptr (8) + 0 członków dla A} = 8 Nie wymaga wypełnienia

od Microsoft

Każdy obiekt danych ma wymaganie wyrównania. W przypadku konstrukcji wymóg jest największy z jego członków. Każdemu obiektowi przypisane jest przesunięcie, dzięki czemu przesunięcie% wymagania wyrównania == 0

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

EDYTOWAĆ:

oto moje demo:

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';

}

wynik:

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

jak widać, a & b nigdy nie nakładają się na siebie, a przy vptr przy wielokrotnym dziedziczeniu każdy ma swoją własną wartość wskaźnika

Uwaga skompilowana przez kompilację VC2019 x64


Nie sądzę, że tak to działa. Mimo że Anie ma członków, nadal zajmuje 1 bajt (który może być współdzielony z innym obiektem). In E, Anie znajduje się po vptr; nakłada się na pierwszy bajt vptr. (Oto wersja demo ; zmodyfikowałem nieco kod, aby był Adostępny). To samo dzieje się w przypadku pierwszego Aw F. Ponieważ A(i B) można umieścić na vptr, nie jestem pewien, dlaczego tak się nie dzieje B.
HolyBlackCat

@HolyBlackCat, ale tak się stało, sprawdź kod testu
Ahmed Anter

Aha, więc MSVC zachowuje się tutaj inaczej niż GCC / Clang; nie jest wystarczająco inteligentny, aby umieścić Ana vptr. To może wyjaśnić, dlaczego w MSVC jest 16, ale nie jestem pewien, co się dzieje z GCC i Clang.
HolyBlackCat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.