Quando você não deve usar destruidores virtuais?

98

Existe uma boa razão para não declarar um destruidor virtual para uma classe? Quando você deve especificamente evitar escrever um?

Mag Roader
fonte

Respostas:

72

Não há necessidade de usar um destruidor virtual quando qualquer uma das opções abaixo for verdadeira:

  • Sem intenção de derivar classes dele
  • Sem instanciação na pilha
  • Nenhuma intenção de armazenar em um ponteiro de uma superclasse

Nenhuma razão específica para evitá-lo, a menos que você esteja realmente tão pressionado por memória.

set
fonte
25
Esta não é uma boa resposta. "Não há necessidade" é diferente de "não deveria" e "nenhuma intenção" é diferente de "tornado impossível".
Programador Windows de
5
Adicione também: nenhuma intenção de excluir uma instância por meio de um ponteiro de classe base.
Adam Rosenfield
9
Isso realmente não responde à pergunta. Onde está o seu bom motivo para não usar um dtor virtual?
mxcl de
9
Acho que quando não há necessidade de fazer algo, esse é um bom motivo para não o fazer. Está seguindo o princípio de Design Simples do XP.
setembro de
12
Ao dizer que você "não tem intenção", você está fazendo uma grande suposição sobre como sua classe será usada. Parece-me que a solução mais simples na maioria dos casos (que deve, portanto, o padrão) deveria ser ter destruidores virtuais e apenas evitá-los se você tiver uma razão específica para não fazê-lo. Ainda estou curioso para saber qual seria um bom motivo.
ckarras
68

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 =defaultsignificará 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
}
Richard Corden
fonte
1
Você está certo e eu estava errado, desempenho não é a única razão. Mas isso mostra que eu estava certo sobre o resto: é melhor que o programador da classe inclua o código para evitar que a classe seja herdada por outra pessoa.
Programador Windows de
querido Richard, você poderia comentar um pouco mais sobre o que escreveu. Não entendi seu ponto, mas parece que o único ponto valioso que encontrei pesquisando no Google) Ou você pode fornecer um link para uma explicação mais detalhada?
John Smith
1
@JohnSmith Eu atualizei a resposta. Espero que isso ajude.
Richard Corden
27

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.

Andy
fonte
3
E, de fato, há uma opção de aviso no gcc que avisa exatamente nesse caso (métodos virtuais, mas sem dtor virtual).
CesarB de
6
Você não corre o risco de perder memória se deriva da classe, independentemente de ter outras funções virtuais?
Mag Roader
1
Eu concordo com a mag. Este uso de um destruidor virtual e / ou método virtual são requisitos separados. O destruidor virtual fornece a capacidade de uma classe de realizar uma limpeza (por exemplo, deletar memória, fechar arquivos, etc ...) E também garante que os construtores de todos os seus membros sejam chamados.
user48956
7

Um destruidor virtual é necessário sempre que houver alguma chance de que deletepossa 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 Bseja 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 deletemuitos 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.

Jay Conrod
fonte
"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." deve ser enfatizado mais em cada resposta que vejo
csguy
5

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.

mxcl
fonte
1
Apenas picuinhas, mas hoje em dia um ponteiro costuma ser de 64 bits em vez de 32.
Head Geek
5

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.

Steve Jessop
fonte
2
O uso polimórfico não implica exclusão polimórfica. Existem muitos casos de uso para uma classe ter métodos virtuais, mas nenhum destruidor virtual. Considere uma caixa de diálogo definida estaticamente típica, em praticamente qualquer kit de ferramentas de GUI. A janela pai destruirá os objetos filho e sabe o tipo exato de cada um, mas todas as janelas filho também serão usadas polimorficamente em qualquer número de lugares, como teste de clique, desenho, APIs de acessibilidade que buscam o texto para texto- motores de fala, etc.
Ben Voigt,
4
Verdade, mas o questionador está perguntando quando você deve evitar especificamente um destruidor virtual. Para a caixa de diálogo que você descreve, um destruidor virtual não faz sentido, mas o IMO não é prejudicial. Não tenho certeza de que nunca precisarei excluir uma caixa de diálogo usando um ponteiro de classe base - por exemplo, posso no futuro querer que minha janela pai crie seus objetos filho usando fábricas. Portanto, não é uma questão de evitar o destruidor virtual, apenas que você pode não se incomodar em ter um. Porém, um destruidor virtual em uma classe não adequada para derivação é prejudicial, porque é enganoso.
Steve Jessop
4

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!"

Kidfisto
fonte
3

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.

Mark Ransom
fonte
1

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.

Jørn Jensen
fonte
1

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.

Lisa
fonte
0

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 .

crime de gelo
fonte
-7

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ê.

Programador Windows
fonte
O polimorfismo certamente tornará as coisas mais lentas. Compare com uma situação em que precisamos de polimorfismo e optamos por não fazê-lo, será ainda mais lento. Exemplo: implementamos toda a lógica no destruidor da classe base, usando RTTI e uma instrução switch para limpar recursos.
setembro de
1
Em C ++, não é sua responsabilidade me impedir de herdar de suas classes que você documentou não são adequadas para uso como classes base. É minha responsabilidade usar a herança com cuidado. A menos que o guia de estilo da casa diga o contrário, é claro.
Steve Jessop
1
... apenas tornar o destruidor virtual não significa que a classe necessariamente funcionará corretamente como uma classe base. Portanto, marcá-lo como virtual "só porque", em vez de fazer essa avaliação, é preencher um cheque que meu código não pode descontar.
Steve Jessop