Qual é o custo do uso de destruidores virtuais, se eu o usar, mesmo que não seja necessário?
O custo da introdução de qualquer função virtual em uma classe (herdada ou parte da definição da classe) é um custo inicial possivelmente muito íngreme (ou não, dependendo do objeto) de um ponteiro virtual armazenado por objeto, como:
struct Integer
{
virtual ~Integer() {}
int value;
};
Nesse caso, o custo da memória é relativamente enorme. O tamanho real da memória de uma instância de classe agora se parece com isso em arquiteturas de 64 bits:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
O total é de 16 bytes para esta Integer
classe, em oposição a meros 4 bytes. Se armazenarmos um milhão deles em uma matriz, teremos 16 megabytes de uso de memória: o dobro do tamanho do cache típico da CPU L3 de 8 MB e a iteração através dessa matriz pode ser muitas vezes mais lenta que o equivalente a 4 megabytes sem o ponteiro virtual como resultado de falhas adicionais de cache e falhas de página.
Esse custo do ponteiro virtual por objeto, no entanto, não aumenta com mais funções virtuais. Você pode ter 100 funções-membro virtuais em uma classe e a sobrecarga por instância ainda seria um único ponteiro virtual.
O ponteiro virtual é normalmente a preocupação mais imediata do ponto de vista de sobrecarga. No entanto, além de um ponteiro virtual por instância, há um custo por classe. Cada classe com funções virtuais gera uma vtable
memória que armazena endereços para as funções que realmente deve chamar (despacho virtual / dinâmico) quando uma chamada de função virtual é feita. O vptr
armazenado por instância, em seguida, aponta para essa classe específica vtable
. Essa sobrecarga geralmente é uma preocupação menor, mas pode aumentar o tamanho binário e adicionar um pouco de custo de tempo de execução se essa sobrecarga for paga desnecessariamente por mil classes em uma base de código complexa, por exemplo, esse vtable
lado do custo na verdade aumenta proporcionalmente com mais e mais funções virtuais no mix.
Os desenvolvedores Java que trabalham em áreas críticas de desempenho entendem muito bem esse tipo de sobrecarga (embora muitas vezes descrito no contexto do boxe), pois um tipo definido pelo usuário Java herda implicitamente de uma object
classe base central e todas as funções em Java são implicitamente virtuais (substituíveis ) na natureza, salvo indicação em contrário. Como resultado, um Java Integer
também tende a exigir 16 bytes de memória em plataformas de 64 bits como resultado desses vptr
metadados de estilo associados por instância, e normalmente é impossível no Java agrupar algo como um único int
em uma classe sem pagar um tempo de execução custo de desempenho por isso.
Então a pergunta é: Por que o c ++ não define todos os destruidores virtuais por padrão?
O C ++ realmente favorece o desempenho com uma mentalidade do tipo "pague conforme o uso" e também ainda muitos projetos de hardware bare metal herdados do C. Ele não deseja incluir desnecessariamente a sobrecarga necessária para a geração de vtable e o envio dinâmico para cada classe / instância envolvida. Se o desempenho não é um dos principais motivos pelos quais você está usando uma linguagem como C ++, você pode se beneficiar mais de outras linguagens de programação existentes, pois grande parte da linguagem C ++ é menos segura e mais difícil do que seria ideal quando o desempenho costuma ser a principal razão para favorecer tal design.
Quando NÃO preciso usar destruidores virtuais?
Com bastante frequência. Se uma classe não for projetada para ser herdada, ela não precisará de um destruidor virtual e só acabará pagando uma sobrecarga possivelmente grande por algo que não precisa. Da mesma forma, mesmo que uma classe seja projetada para ser herdada, mas você nunca exclua instâncias de subtipo por meio de um ponteiro base, também não precisará de um destruidor virtual. Nesse caso, uma prática segura é definir um destruidor não virtual protegido, assim:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
Nesse caso, NÃO devo usar destruidores virtuais?
Na verdade, é mais fácil abordar quando você deve usar destruidores virtuais. Muitas vezes, muito mais classes na sua base de código não serão projetadas para herança.
std::vector
, por exemplo, não foi projetado para ser herdado e normalmente não deve ser herdado (design muito instável), pois isso será propenso a esse problema de exclusão de ponteiro base ( std::vector
evita deliberadamente um destruidor virtual), além de problemas desajeitados de corte de objetos se o seu classe derivada adiciona qualquer novo estado.
Em geral, uma classe herdada deve ter um destruidor público virtual ou um destruidor não-virtual protegido. De C++ Coding Standards
, capítulo 50:
50. Torne os destruidores da classe base públicos e virtuais, ou protegidos e não virtuais. Excluir ou não excluir; eis a questão: se a exclusão através de um ponteiro para uma base base deve ser permitida, o destruidor da base deve ser público e virtual. Caso contrário, deve ser protegido e não virtual.
Uma das coisas que o C ++ tende a enfatizar implicitamente (porque os projetos tendem a ficar realmente quebradiços e desajeitados e possivelmente até mesmo inseguros) é a ideia de que a herança não é um mecanismo projetado para ser usado como uma reflexão tardia. É um mecanismo de extensibilidade com polimorfismo em mente, mas que exige uma previsão de onde a extensibilidade é necessária. Como resultado, suas classes base devem ser projetadas como raízes de uma hierarquia de herança antecipadamente, e não algo que você herda mais tarde como uma reflexão tardia sem essa previsão antecipada.
Nos casos em que você simplesmente deseja herdar para reutilizar o código existente, a composição geralmente é fortemente incentivada (Princípio de Reutilização Composto).