Por que o armazenamento de uma base vazia duplicada não se sobrepõe a um ponteiro vtable?


11

Considere este exemplo:

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

(corra em godbolt)

Aqui você pode ver que, para struct Ea classe base vazia (que tem 1 byte de largura), usa o mesmo armazenamento que o ponteiro vtable, conforme o esperado.

Mas para struct F, que tem uma base vazia duplicada, isso não acontece. O que causa isso?

Eu obtenho o mesmo resultado no GCC, Clang e MSVC. Os resultados acima são para x64, então sizeof(void *) == 8.


Curiosamente, o struct G : A, B {void *ptr;};GCC e o Clang realizam EBO (o tamanho é 8), mas o MSVC não (o tamanho é 16).


3
Estranhamente, por herança de C(que herdam A, B), obtém resultado diferente do que herdar forma Ae Bdiretamente
Guillaume Racicot

11
Eu gostei de pesquisar este. Obrigado pela pergunta e pelo link. Não tenho certeza se tenho uma resposta e, portanto, vou simplesmente comentar. Será que isso decorre da ambiguidade introduzida a partir da derivação de Ce F? Afinal, 2 * sizeof(void*) == 16no x86_64, como você disse. O compilador não pode otimizar completamente (como o Story Teller disse) e não.
Andrew Falanga

2
É normal que você obtenha o mesmo resultado no gcc e no clang, já que ambos seguem o itanium ABI. E, se esse for o caso, acho que, ao definir a ABI, eles temiam que o algoritmo de layout pudesse se tornar muito caro, por isso usaram alguns atalhos (também conhecidos como pessimizações).
Marc Glisse

2
@RianQuinn Uma base duplicada não invalida uma estrutura.
HolyBlackCat

11
@RianQuinn que herda da mesma classe várias vezes por diferentes "caminhos" é perfeitamente válido em C ++. Se você deseja criar uma estrutura de diamante, ou seja, ter a classe base apenas uma vez, é necessário usar a herança virtual. Mas se você não quer um diamante e ter uma classe base duplicada não é problema para você, também não é um problema para o idioma. O código do OP produz apenas um aviso, dizendo que o segundo A, herdado por B, não pode ser acessado. Está bem. Somente se você realmente tentar acessá-lo, como no exemplo, você receberá um erro.
sebrockm

Respostas:


4

Como o compilador adiciona um preenchimento de byte após struct A

F {vptr (8) + 0 membros do preenchimento A + 1 (porque A está vazio) +0 de b} = 9, o compilador adiciona um preenchimento de 7 bytes para alinhar o armazenamento da estrutura;

E {vptr (8) + 0 membros para A} = 8 Não é necessário preenchimento

da Microsoft

Todo objeto de dados tem um requisito de alinhamento. Para estruturas, o requisito é o maior de seus membros. A cada objeto é atribuído um deslocamento para que o deslocamento% de alinhamento-requisito == 0

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

EDITAR:

aqui está minha 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';

}

resultado:

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

como você pode ver, a & b nunca se sobrepõe e, com o vptr em herança múltipla, cada um tem seu próprio valor de ponteiro

nota compilada pelo VC2019 x64 build


Eu não acho que é assim que funciona. Mesmo Asem membros, ele ainda ocupa 1 byte (que pode ser compartilhado com outro objeto). In E, Anão está localizado após o vptr; sobrepõe o primeiro byte do vptr. (Aqui está uma demonstração ; modifiquei ligeiramente o código para torná-lo Aacessível). Mesmo acontece pela primeira Aem F. Como A(e B) pode ser colocado no topo do vptr, não sei por que isso não acontece B.
HolyBlackCat

@HolyBlackCat mas isso é o código de teste de seleção aconteceu
Ahmed Anter

Ah, então o MSVC se comporta de maneira diferente do GCC / Clang aqui; não é inteligente o suficiente para colocar Aem cima do vptr. Isso pode explicar por que a saída é 16 no MSVC, mas não tenho certeza do que está acontecendo com o GCC & Clang.
HolyBlackCat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.