Covariância do contêiner em C ++

Eu sei que o C ++ não suporta covariância para elementos de contêineres, como em Java ou C #. Então, o seguinte código provavelmente é um comportamento indefinido:

#include  struct A {}; struct B : A {}; std::vector test; std::vector* foo = reinterpret_cast<std::vector*>(&test); 

Não surpreendentemente, recebi votos negativos ao sugerir isso como uma solução para outra questão .

Mas que parte do padrão C ++ me diz exatamente que isso resultará em um comportamento indefinido? É garantido que ambos std::vector std::vector e std::vector armazenam seus pointers em um bloco contíguo de memory. Também é garantido que sizeof(A*) == sizeof(B*) . Finalmente, A* a = new B é perfeitamente legal.

Então, que maus espíritos no padrão eu conjurei (exceto estilo)?

A regra violada aqui está documentada em C ++ 03 3.10 / 15 [basic.lval], que especifica o que é referido informalmente como a “regra de aliasing restrita”

Se um programa tentar acessar o valor armazenado de um object por meio de um lvalue diferente de um dos seguintes tipos, o comportamento é indefinido:

  • o tipo dynamic do object,

  • uma versão qualificada em cv do tipo dynamic do object,

  • um tipo que é o tipo assinado ou não assinado correspondente ao tipo dynamic do object,

  • um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada em cv do tipo dynamic do object,

  • um tipo agregado ou sindical que inclua um dos tipos acima mencionados entre seus membros (incluindo, recursivamente, um membro de um sindicato subagregado ou contido),

  • um tipo que é um tipo de class base (possivelmente qualificado por cv) do tipo dynamic do object,

  • um tipo de caractere char ou não assinado.

Em suma, dado um object, você só tem permissão para acessar esse object por meio de uma expressão que tenha um dos tipos na lista. Para um object do tipo de class que não possui classs base, como std::vector , basicamente você está limitado aos tipos nomeados no primeiro, no segundo e no último marcador.

std::vector e std::vector são tipos inteiramente não relacionados e você não pode usar um object do tipo std::vector como se fosse um std::vector O compilador poderia fazer todo tipo de coisa se você violar esta regra, incluindo:

  • realizar otimizações diferentes em um do que no outro, ou

  • esquematizar os membros internos de um diferente, ou

  • execute otimizações assumindo que um std::vector* nunca pode se referir ao mesmo object que um std::vector*

  • usar verificações de tempo de execução para garantir que você não esteja violando a regra de alias restrita

[Ele também pode não fazer nada disso e pode “funcionar”, mas não há garantia de que ele “funcionará” e se você alterar compiladores ou versões do compilador ou configurações de compilation, tudo poderá parar de “funcionar”. Eu uso as citações do susto por uma razão aqui. :-)]

Mesmo se você tivesse uma Base*[N] não poderia usar essa matriz como se fosse uma Derived*[N] (embora nesse caso, o uso provavelmente seria mais seguro, onde “mais seguro” significa “ainda indefinido mas menos provável que você tenha problemas.

Você está invocando o mau espírito de reinterpret_cast <>.

A menos que você realmente saiba o que faz (refiro-me não orgulhosamente e não pedantemente), reinterpret_cast é um dos portais do mal.

O único uso seguro que eu conheço é o gerenciamento de classs e estruturas entre as chamadas de funções C ++ e C. Há talvez alguns outros no entanto.

O problema geral com covariância em contêineres é o seguinte:

Digamos que seu casting funcione e seja legal (não é, mas vamos supor que seja para o exemplo a seguir):

 #include  struct A {}; struct B : A { public: int Method(int x, int z); }; struct C : A { public: bool Method(char y); }; std::vector test; std::vector* foo = reinterpret_cast*>(&test); foo->push_back(new C); test[0]->Method(7, 99); // What should happen here??? 

Então você também reinterpretou um C * para um B * …

Na verdade, não sei como o .NET e o Java gerenciam isso (acho que eles lançam uma exceção ao tentar inserir um C).

Eu acho que será mais fácil mostrar do que contar:

 struct A { int a; }; struct Stranger { int a; }; struct B: Stranger, A {}; int main(int argc, char* argv[]) { B someObject; B* b = &someObject; A* correct = b; A* incorrect = reinterpret_cast(b); assert(correct != incorrect); // troubling, isn't it ? return 0; } 

O problema (específico) mostrado aqui é que, ao fazer uma conversão “adequada”, o compilador adiciona algum ajuste de ponteiro, dependendo do layout de memory dos objects. Em um reinterpret_cast , nenhum ajuste é realizado.

Eu suponho que você entenderá porque o uso de reinterpet_cast normalmente deveria ser banido do código …