Por que a otimização de base vazia é proibida quando a classe base vazia também é uma variável de membro?

14

A otimização da base vazia é ótima. No entanto, ele vem com a seguinte restrição:

A otimização de base vazia é proibida se uma das classes de base vazias também for o tipo ou a base do tipo do primeiro membro de dados não estático, pois os dois subobjetos de base do mesmo tipo precisam ter endereços diferentes na representação do objeto do tipo mais derivado.

Para explicar essa restrição, considere o seguinte código. O static_assertirá falhar. Considerando que, alterar Fooou Barherdar em vez disso Base2evitará o erro:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

static_assert(offsetof(Bar,foo)==0,"Error!");

Eu entendo esse comportamento completamente. O que não entendo é por que esse comportamento específico existe . Obviamente, foi adicionado por um motivo, pois é uma adição explícita, não uma supervisão. Qual é a justificativa para isso?

Em particular, por que os dois subobjetos de base devem ter endereços diferentes? No acima, Baré um tipo e fooé uma variável de membro desse tipo. Não vejo por que a classe base Barimporta para a classe base do tipo de foo, ou vice-versa.

De fato, eu espero que &fooseja o mesmo que o endereço da Barinstância que o contém - como é necessário em outras situações (1) . Afinal, eu não estou fazendo nada sofisticado com virtualherança, as classes base estão vazias de qualquer maneira, e a compilação Base2mostra que nada quebra nesse caso específico.

Mas claramente esse raciocínio está incorreto de alguma maneira, e há outras situações em que essa limitação seria necessária.

Digamos que as respostas devam ser para o C ++ 11 ou mais recente (atualmente estou usando o C ++ 17).

(1) Nota: O EBO foi atualizado no C ++ 11 e, em particular, tornou-se obrigatório para StandardLayoutTypes (embora Bar, acima, não seja a StandardLayoutType).

imallett
fonte
4
Como a lógica que você citou (" como os dois subobjetos de base do mesmo tipo são obrigados a ter endereços diferentes ") fica aquém? Objetos diferentes do mesmo tipo são necessários para ter endereços distintos, e esse requisito garante que não violemos essa regra. Se a otimização da base vazia for aplicada aqui, poderíamos ter Base *a = new Bar(); Base *b = a->foo;com a==b, mas ae bclaramente objetos diferentes (talvez com substituições de métodos virtuais diferentes).
Toby Speight
11
A resposta do advogado do idioma está citando as partes relevantes da especificação. E parece que você já sabe disso.
Deduplicator
3
Não sei se entendi que tipo de resposta você está procurando aqui. O modelo de objeto C ++ é o que é. A restrição existe porque o modelo de objeto exige isso. O que mais você procura além disso?
Nicol Bolas 8/01
@TobySpeight É necessário que objetos diferentes do mesmo tipo tenham endereços distintos. É fácil quebrar essa regra em um programa com comportamento bem definido.
Language Lawyer
@TobySpeight Não, não quero dizer que você esqueceu de dizer sobre a vida: "Objeto diferente do mesmo tipo na vida útil " . É possível ter vários objetos do mesmo tipo, todos vivos, no mesmo endereço. Existem pelo menos 2 erros no texto que permitem isso.
Language Lawyer

Respostas:

4

Ok, parece que eu errei o tempo todo, pois, para todos os meus exemplos, é necessário que exista uma vtable para o objeto base, o que impediria a otimização da base vazia. Deixarei que os exemplos permaneçam, pois acho que eles dão alguns exemplos interessantes de por que endereços únicos são normalmente uma coisa boa de se ter.

Tendo estudado tudo isso mais profundamente, não há razão técnica para a otimização da classe base vazia ser desativada quando o primeiro membro é do mesmo tipo que a classe base vazia. Isso é apenas uma propriedade do atual modelo de objeto C ++.

Porém, com o C ++ 20, haverá um novo atributo [[no_unique_address]]que informa ao compilador que um membro de dados não estático pode não precisar de um endereço exclusivo (tecnicamente falando, ele está potencialmente sobreposto [intro.object] / 7 ).

Isso implica que (ênfase minha)

O membro de dados não estáticos pode compartilhar o endereço de outro membro de dados não estáticos ou o de uma classe base , [...]

portanto, é possível "reativar" a otimização da classe base vazia, atribuindo o atributo ao primeiro membro de dados [[no_unique_address]]. Adicionei um exemplo aqui que mostra como isso (e todos os outros casos em que eu conseguia pensar) funciona.

Exemplos errados de problemas através deste

Como parece que uma classe vazia pode não ter métodos virtuais, deixe-me adicionar um terceiro exemplo:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

Mas as duas últimas ligações são iguais.

Exemplos antigos (provavelmente não respondem à pergunta, pois as classes vazias podem não conter métodos virtuais, ao que parece)

Considere no seu código acima (com destruidores virtuais adicionados) o exemplo a seguir

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

Mas como o compilador deve distinguir esses dois casos?

E talvez um pouco menos artificial:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

Mas os dois últimos são os mesmos se tivermos otimização de classe base vazia!

n314159
fonte
11
Pode-se argumentar que a segunda chamada tem um comportamento indefinido, de qualquer maneira. Portanto, o compilador não precisa distinguir nada.
StoryTeller - Unslander Monica
11
Uma classe com qualquer membro virtual não está vazia, portanto, é irrelevante aqui!
Deduplicator
11
@Duplicador Você tem uma cotação padrão sobre isso? O Cppref nos diz que uma classe vazia é "uma classe ou estrutura que não possui membros de dados não estáticos".
n314159 8/01
11
@ n314159 std::is_emptyna cppreference é muito mais elaborado. Mesmo a partir do actual projecto em eel.is .
Deduplicator
2
Você não pode dynamic_castquando não é polimórfico (com pequenas exceções não relevantes aqui).
TC