Herança
O ponto principal da herança é compartilhar uma interface e protocolo comuns entre muitas implementações diferentes, de modo que uma instância de uma classe derivada possa ser tratada de forma idêntica a qualquer outra instância de qualquer outro tipo derivado.
No C ++, a herança também traz detalhes de implementação, marcando (ou não marcando) o destruidor como virtual é um desses detalhes de implementação.
Função Vinculação
Agora, quando uma função, ou qualquer um de seus casos especiais, como um construtor ou destruidor, é chamada, o compilador deve escolher qual implementação de função se destina. Em seguida, ele deve gerar código de máquina que siga essa intenção.
A maneira mais simples de trabalhar isso seria selecionar a função em tempo de compilação e emitir código de máquina suficiente para que, independentemente de quaisquer valores, quando esse trecho de código for executado, ele sempre execute o código da função. Isso funciona muito bem, exceto pela herança.
Se tivermos uma classe base com uma função (poderia ser qualquer função, incluindo o construtor ou destruidor) e seu código chamar uma função, o que isso significa?
Tomando como exemplo, se você chamou initialize_vector()
o compilador, deve decidir se realmente deseja chamar a implementação encontrada em Base
, ou a implementação encontrada em Derived
. Existem duas maneiras de decidir isso:
- A primeira é decidir que, como você chamou de um
Base
tipo, você quis dizer a implementação Base
.
- A segunda é decidir que, porque o tipo de tempo de execução do valor armazenado no
Base
valor digitado pode ser Base
, ou Derived
que a decisão sobre qual chamada será feita, deve ser tomada no tempo de execução quando chamada (sempre que é chamada).
O compilador neste momento está confuso, ambas as opções são igualmente válidas. É quando virtual
entra em cena. Quando essa palavra-chave está presente, o compilador escolhe a opção 2, atrasando a decisão entre todas as implementações possíveis até que o código seja executado com um valor real. Quando essa palavra-chave está ausente, o compilador escolhe a opção 1 porque esse é o comportamento normal.
O compilador ainda pode escolher a opção 1 no caso de uma chamada de função virtual. Mas somente se puder provar que esse é sempre o caso.
Construtores e Destrutores
Então, por que não especificamos um construtor virtual?
Mais intuitivamente, como o compilador escolheria entre implementações idênticas do construtor para Derived
e Derived2
? Isso é bem simples, não pode. Não há valor preexistente a partir do qual o compilador possa aprender o que realmente era pretendido. Não há valor pré-existente porque esse é o trabalho do construtor.
Então, por que precisamos especificar um destruidor virtual?
Mais intuitivamente, como o compilador escolheria entre implementações para Base
e Derived
? São apenas chamadas de função, portanto, o comportamento da chamada de função acontece. Sem um destruidor virtual declarado, o compilador decidirá ligar diretamente ao Base
destruidor, independentemente do tipo de tempo de execução dos valores.
Em muitos compiladores, se o derivado não declarar nenhum membro de dados nem herdar de outros tipos, o comportamento no ~Base()
será adequado, mas não é garantido. Funcionaria puramente por acaso, como estar diante de um lança-chamas que ainda não havia sido aceso. Você está bem por um tempo.
A única maneira correta de declarar qualquer tipo de base ou interface em C ++ é declarar um destruidor virtual, para que o destruidor correto seja chamado para qualquer instância específica da hierarquia de tipos desse tipo. Isso permite que a função com mais conhecimento da instância limpe essa instância corretamente.
~derived()
que delega ao destruidor do vec. Como alternativa, você está assumindo queunique_ptr<base> pt
conheceria o destruidor derivado. Sem um método virtual, este não pode ser o caso. Enquanto um unique_ptr pode receber uma função de exclusão que é um parâmetro de modelo sem nenhuma representação de tempo de execução, e esse recurso não serve para esse código.