¿Por qué el almacenamiento para una base vacía duplicada no se superpone con un puntero vtable?


11

Considere este ejemplo:

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

(Ejecutar en Godbolt)

Aquí puede ver que para struct Ela clase base vacía (que es 1 byte grande) usa el mismo almacenamiento que el puntero vtable, como se esperaba.

Pero para struct F, que tiene una base vacía duplicada, esto no sucede. ¿Qué causa esto?

Obtengo el mismo resultado en GCC, Clang y MSVC. Los resultados anteriores son para x64, entonces sizeof(void *) == 8.


Curiosamente, para struct G : A, B {void *ptr;};GCC y Clang realizan EBO (el tamaño es 8), pero MSVC no (el tamaño es 16).


3
Extrañamente, heredando de C(que heredan de A, B) se obtiene resultado diferente que heredar forma Ay Bdirectamente
Guillaume Racicot

1
He disfrutado investigando este. Gracias por la pregunta y el enlace. No estoy seguro de tener una respuesta, por lo que simplemente comentaré. ¿Podría ser que esto surge de la ambigüedad introducida por la derivación de Cy F? Después de todo, 2 * sizeof(void*) == 16en x86_64 como has dicho. El compilador no puede optimizar completamente (como dijo Story Teller) y tampoco lo hace.
Andrew Falanga

2
Es normal que obtenga el mismo resultado en gcc y clang, ya que ambos siguen el itanium ABI. Y si este es el caso, creo, al definir el ABI, temían que el algoritmo de diseño pudiera ser demasiado costoso, por lo que tomaron algunos atajos (también conocidos como pesimismos).
Marc Glisse

2
@RianQuinn Una base duplicada no invalida una estructura.
HolyBlackCat

1
@RianQuinn que hereda de la misma clase varias veces a través de diferentes "rutas" es perfectamente válido en C ++. Si desea crear una estructura de diamante, es decir, tener la clase base solo una vez, debe usar la herencia virtual. Pero si no quiere un diamante y tener una clase base duplicada no es un problema para usted, entonces tampoco es un problema para el idioma. El código de OP solo produce una advertencia, diciendo que no se puede acceder al segundo A, heredado vía B. Eso está bien. Solo si realmente intentas acceder a él, como en tu ejemplo, obtienes un error.
sebrockm

Respuestas:


4

Porque el compilador agrega un relleno de byte después de la estructura A

F {vptr (8) + 0 miembros de A + 1 relleno (porque A está vacío) +0 de b} = 9 luego el compilador agrega 7 bytes de relleno para alinear el almacenamiento de la estructura;

E {vptr (8) + 0 miembros para A} = 8 No se requiere relleno

de Microsoft

Cada objeto de datos tiene un requisito de alineación. Para estructuras, el requisito es el mayor de sus miembros. A cada objeto se le asigna un desplazamiento para que el desplazamiento% requisito de alineación == 0

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

EDITAR:

Aquí está mi 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';

}

salida:

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

como puede ver, a & b nunca se superpone entre sí y con vptr en herencia múltiple cada uno tiene su propio valor de puntero

nota compilada por VC2019 x64 build


No creo que así sea como funciona. Aunque Ano tiene miembros, todavía ocupa 1 byte (que se puede compartir con otro objeto). En E, Ano se encuentra después de vptr; se superpone al primer byte de vptr. (Aquí hay una demostración ; he modificado ligeramente el código para que sea Aaccesible). Lo mismo sucede por primera Aen F. Como A(y B) se puede colocar encima del vptr, no estoy seguro de por qué no sucede B.
HolyBlackCat

@HolyBlackCat pero esto es lo que sucedió verifique el código de prueba
Ahmed Anter

Ajá, entonces MSVC se comporta de manera diferente a GCC / Clang aquí; no es lo suficientemente inteligente como para ponerlo Aencima del vptr. Puede explicar por qué la salida es 16 en MSVC, pero no estoy seguro de qué está pasando con GCC y Clang.
HolyBlackCat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.