Existe uma boa razão para não declarar um destruidor virtual para uma classe? Quando você deve especificamente evitar escrever um?
Existe uma boa razão para não declarar um destruidor virtual para uma classe? Quando você deve especificamente evitar escrever um?
Respostas:
Não há necessidade de usar um destruidor virtual quando qualquer uma das opções abaixo for verdadeira:
Nenhuma razão específica para evitá-lo, a menos que você esteja realmente tão pressionado por memória.
Para responder à pergunta explicitamente, ou seja, quando você não deve declarar um destruidor virtual.
C ++ '98 / '03
Adicionar um destruidor virtual pode alterar sua classe de POD (dados antigos simples) * ou agregada para não-POD. Isso pode impedir que seu projeto seja compilado se o tipo de classe for inicializado por agregação em algum lugar.
struct A {
// virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // Will fail if virtual dtor declared
}
Em um caso extremo, tal mudança também pode causar um comportamento indefinido, onde a classe está sendo usada de uma forma que requer um POD, por exemplo, passando-o por meio de um parâmetro de reticências ou usando-o com memcpy.
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* Um tipo POD é um tipo que tem garantias específicas sobre seu layout de memória. O padrão realmente diz apenas que se você copiar de um objeto com tipo POD em uma matriz de caracteres (ou caracteres não assinados) e vice-versa, o resultado será o mesmo do objeto original.]
C ++ moderno
Em versões recentes de C ++, o conceito de POD foi dividido entre o layout da classe e sua construção, cópia e destruição.
Para o caso de reticências, não é mais um comportamento indefinido, agora é suportado condicionalmente com semântica definida pela implementação (N3937 - ~ C ++ '14 - 5.2.2 / 7):
... Passar um argumento potencialmente avaliado do tipo de classe (Cláusula 9) tendo um construtor de cópia não trivial, um construtor de movimento não trivial ou um destruidor on-trivial, sem nenhum parâmetro correspondente, é suportado condicionalmente com implementação- semântica definida.
Declarar um destruidor diferente de =default
significará que não é trivial (12.4 / 5)
... Um destruidor é trivial se não for fornecido pelo usuário ...
Outras mudanças no C ++ moderno reduzem o impacto do problema de inicialização de agregação, pois um construtor pode ser adicionado:
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
Eu declaro um destruidor virtual se e somente se eu tiver métodos virtuais. Depois de ter métodos virtuais, não confio em mim mesmo para evitar instanciar isso no heap ou armazenar um ponteiro para a classe base. Ambos são operações extremamente comuns e muitas vezes vazam recursos silenciosamente se o destruidor não for declarado virtual.
Um destruidor virtual é necessário sempre que houver alguma chance de que delete
possa ser chamado em um ponteiro para um objeto de uma subclasse com o tipo de sua classe. Isso garante que o destruidor correto seja chamado em tempo de execução sem que o compilador precise saber a classe de um objeto no heap em tempo de compilação. Por exemplo, suponha que B
seja uma subclasse de A
:
A *x = new B;
delete x; // ~B() called, even though x has type A*
Se o seu código não for crítico para o desempenho, seria razoável adicionar um destruidor virtual a cada classe base que você escrever, apenas por segurança.
No entanto, se você encontrar delete
muitos objetos em um loop fechado, a sobrecarga de desempenho de chamar uma função virtual (mesmo uma que esteja vazia) pode ser perceptível. O compilador geralmente não consegue embutir essas chamadas, e o processador pode ter dificuldade em prever para onde ir. É improvável que isso tenha um impacto significativo no desempenho, mas vale a pena mencionar.
Funções virtuais significam que cada objeto alocado aumenta no custo de memória por um ponteiro de tabela de função virtual.
Portanto, se seu programa envolver a alocação de um número muito grande de algum objeto, convém evitar todas as funções virtuais para salvar os 32 bits adicionais por objeto.
Em todos os outros casos, você evitará problemas de depuração para tornar o dtor virtual.
Nem todas as classes C ++ são adequadas para uso como classe base com polimorfismo dinâmico.
Se você deseja que sua classe seja adequada para polimorfismo dinâmico, seu destruidor deve ser virtual. Além disso, quaisquer métodos que uma subclasse possa concebivelmente desejar substituir (o que pode significar todos os métodos públicos, além de alguns métodos potencialmente protegidos usados internamente) devem ser virtuais.
Se sua classe não for adequada para polimorfismo dinâmico, o destruidor não deve ser marcado como virtual, porque isso é enganoso. Isso apenas encoraja as pessoas a usar sua classe incorretamente.
Aqui está um exemplo de uma classe que não seria adequada para polimorfismo dinâmico, mesmo se seu destruidor fosse virtual:
class MutexLock {
mutex *mtx_;
public:
explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
~MutexLock() { mtx_->unlock(); }
private:
MutexLock(const MutexLock &rhs);
MutexLock &operator=(const MutexLock &rhs);
};
O objetivo dessa aula é sentar na pilha de RAII. Se você está passando ponteiros para objetos desta classe, quanto mais subclasses dela, então você está fazendo isso errado.
Um bom motivo para não declarar um destruidor como virtual é quando isso evita que sua classe tenha uma tabela de função virtual adicionada, e você deve evitar isso sempre que possível.
Eu sei que muitas pessoas preferem apenas sempre declarar os destruidores como virtuais, apenas para ficar no lado seguro. Mas se sua classe não tem nenhuma outra função virtual, então realmente, realmente não há sentido em ter um destruidor virtual. Mesmo se você der sua classe para outras pessoas que derivam outras classes dela, então eles não terão nenhuma razão para chamar delete em um ponteiro que foi atualizado para sua classe - e se o fizerem, eu consideraria isso um bug.
Ok, há uma única exceção, ou seja, se sua classe é (mal-) usada para realizar a exclusão polimórfica de objetos derivados, mas então você - ou os outros caras - esperançosamente sabem que isso requer um destruidor virtual.
Dito de outra forma, se sua classe tiver um destruidor não virtual, esta é uma declaração muito clara: "Não me use para excluir objetos derivados!"
Se você tiver uma classe muito pequena com um grande número de instâncias, a sobrecarga de um ponteiro vtable pode fazer a diferença no uso de memória do seu programa. Desde que sua classe não tenha nenhum outro método virtual, tornar o destruidor não virtual economizará essa sobrecarga.
Eu geralmente declaro o destruidor virtual, mas se você tiver um código crítico de desempenho usado em um loop interno, convém evitar a consulta à tabela virtual. Isso pode ser importante em alguns casos, como verificação de colisão. Mas tome cuidado ao destruir esses objetos se usar herança, ou você destruirá apenas metade do objeto.
Observe que a pesquisa da tabela virtual acontece para um objeto se qualquer método nesse objeto for virtual. Portanto, não adianta remover a especificação virtual de um destruidor se você tiver outros métodos virtuais na classe.
Se você deve garantir de forma absolutamente positiva que sua classe não tenha uma vtable, você não deve ter um destruidor virtual também.
Este é um caso raro, mas acontece.
O exemplo mais familiar de um padrão que faz isso são as classes DirectX D3DVECTOR e D3DMATRIX. Esses são métodos de classe em vez de funções para o açúcar sintático, mas as classes intencionalmente não têm uma vtable para evitar a sobrecarga da função porque essas classes são usadas especificamente no loop interno de muitos aplicativos de alto desempenho.
Na operação que será realizada na classe base, e que deverá se comportar virtualmente, deverá ser virtual. Se a exclusão puder ser realizada polimorficamente por meio da interface da classe base, ela deve se comportar virtualmente e ser virtual.
O destruidor não precisa ser virtual se você não pretende derivar da classe. E mesmo se você fizer isso, um destruidor não virtual protegido é tão bom se a exclusão dos ponteiros da classe base não for necessária .
A resposta do desempenho é a única que conheço que tem chance de ser verdadeira. Se você mediu e descobriu que a virtualização de seus destruidores realmente acelera as coisas, então provavelmente você tem outras coisas nessa classe que precisam ser aceleradas também, mas neste ponto existem considerações mais importantes. Algum dia alguém descobrirá que seu código forneceria uma boa classe base para eles e economizaria o trabalho de uma semana. É melhor você certificar-se de que eles façam o trabalho daquela semana, copiando e colando seu código, em vez de usá-lo como base. É melhor você garantir que alguns de seus métodos importantes sejam privados, para que ninguém possa herdar de você.