Eu acreditava ter pesquisado muitas vezes sobre destruidores virtuais, a maioria menciona o propósito de destruidores virtuais e por que você precisa de destruidores virtuais. Também acho que na maioria dos casos os destruidores precisam ser virtuais.
Então a pergunta é: Por que o c ++ não define todos os destruidores virtuais por padrão? ou em outras perguntas:
Quando NÃO preciso usar destruidores virtuais?
Nesse caso, NÃO devo usar destruidores virtuais?
Qual é o custo do uso de destruidores virtuais, se eu o usar, mesmo que não seja necessário?
c++
virtual-functions
ggrr
fonte
fonte
Respostas:
Se você adicionar um destruidor virtual a uma classe:
na maioria das implementações (todas?) atuais do C ++, toda instância de objeto dessa classe precisa armazenar um ponteiro na tabela de despacho virtual para o tipo de tempo de execução, e essa própria tabela de despacho virtual adicionada à imagem executável
o endereço da tabela de despacho virtual não é necessariamente válido entre os processos, o que pode impedir o compartilhamento seguro desses objetos na memória compartilhada
ter um ponteiro virtual incorporado frustra a criação de uma classe com layout de memória que corresponda a algum formato de entrada ou saída conhecido (por exemplo, para que um
Price_Tick*
possa ser direcionado diretamente para a memória adequadamente alinhada em um pacote UDP recebido e usado para analisar / acessar ou alterar os dados, ou colocarnew
essa classe para gravar dados em um pacote de saída)o destruidor se autodenomina pode - sob certas condições - ter que ser despachado virtualmente e, portanto, fora de linha, enquanto destruidores não virtuais podem ser inline ou otimizados se trivial ou irrelevante para o chamador
O argumento "não projetado para ser herdado de" não seria uma razão prática para nem sempre ter um destruidor virtual se também não fosse pior na prática, como explicado acima; mas, como é pior, esse é um critério importante para pagar o custo: o padrão é ter um destruidor virtual se a sua classe for usada como classe base . Isso nem sempre é necessário, mas garante que as classes na hierarquia possam ser usadas mais livremente sem comportamento acidental indefinido se um destruidor de classe derivado for chamado usando um ponteiro ou referência de classe base.
Não é assim ... muitas classes não têm essa necessidade. Existem muitos exemplos de onde é desnecessário parecer tolo enumerá-los, mas basta olhar através da Biblioteca Padrão ou dizer impulso e você verá que há uma grande maioria de classes que não têm destruidores virtuais. No impulso 1,53, conto 72 destruidores virtuais de 494.
fonte
BTW,
Para classes base com exclusão polimórfica.
fonte
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:
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:
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. Ovptr
armazenado por instância, em seguida, aponta para essa classe específicavtable
. 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, essevtable
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 JavaInteger
também tende a exigir 16 bytes de memória em plataformas de 64 bits como resultado dessesvptr
metadados de estilo associados por instância, e normalmente é impossível no Java agrupar algo como um únicoint
em uma classe sem pagar um tempo de execução custo de desempenho por isso.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.
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:
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: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).
fonte
Por que o c ++ não define todos os destruidores virtuais por padrão? Custo de armazenamento extra e chamada da tabela de método virtual. O C ++ é usado para programação de sistema, baixa latência, rt, onde isso pode ser um fardo.
fonte
Este é um bom exemplo de quando não usar o destruidor virtual: De Scott Meyers:
Se uma classe não contiver nenhuma função virtual, isso geralmente indica que ela não deve ser usada como classe base. Quando uma classe não se destina a ser usada como classe base, tornar o destruidor virtual geralmente é uma má ideia. Considere este exemplo, com base em uma discussão no ARM:
Se um int curto ocupa 16 bits, um objeto Point pode caber em um registro de 32 bits. Além disso, um objeto Point pode ser passado como uma quantidade de 32 bits para funções escritas em outros idiomas, como C ou FORTRAN. Se o destruidor de Point for virtualizado, a situação muda.
No momento em que você adiciona um membro virtual, um ponteiro virtual é adicionado à sua classe que aponta para a tabela virtual dessa classe.
fonte
If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.
Wut. Alguém mais se lembra dos Bons Velhos Dias, onde é permitido usar classes e herança para criar camadas sucessivas de membros e comportamentos reutilizáveis, sem ter que se preocupar com métodos virtuais? Vamos, Scott. Eu entendo o ponto principal, mas esse "frequentemente" está realmente chegando.Um destruidor virtual adiciona um custo de tempo de execução. O custo é especialmente alto se a classe não tiver outros métodos virtuais. O destruidor virtual também é necessário apenas em um cenário específico, em que um objeto é excluído ou destruído por um ponteiro para uma classe base. Nesse caso, o destruidor da classe base deve ser virtual e o destruidor de qualquer classe derivada será implicitamente virtual. Existem alguns cenários em que uma classe base polimórfica é usada de tal maneira que o destruidor não precisa ser virtual:
std::unique_ptr<Derived>
, e o polimorfismo ocorre apenas por referências e ponteiros não proprietários. Outro exemplo é quando os objetos são alocados usandostd::make_shared<Derived>()
. É bom usarstd::shared_ptr<Base>
, contanto que o ponteiro inicial fosse astd::shared_ptr<Derived>
. Isso ocorre porque os ponteiros compartilhados têm seu próprio envio dinâmico para destruidores (o deleter) que não depende necessariamente de um destruidor de classe base virtual.Obviamente, qualquer convenção para usar objetos apenas das maneiras mencionadas acima pode ser facilmente quebrada. Portanto, o conselho de Herb Sutter permanece tão válido como sempre: "Os destruidores da classe base devem ser públicos e virtuais, ou protegidos e não virtuais". Dessa forma, se alguém tentar excluir um ponteiro para uma classe base com destruidor não virtual, provavelmente receberá um erro de violação de acesso no momento da compilação.
Então, novamente, existem classes que não foram projetadas para serem classes base (públicas). Minha recomendação pessoal é fazê-los
final
em C ++ 11 ou superior. Se ele foi projetado para ser um peg quadrado, é provável que não funcione muito bem como um peg redondo. Isso está relacionado à minha preferência por ter um contrato de herança explícito entre a classe base e a classe derivada, para o padrão de design da NVI (interface não virtual), para classes base abstratas, em vez de concretas, e para a minha aversão a variáveis-membro protegidas, entre outras coisas. , mas sei que todas essas visualizações são controversas até certo ponto.fonte
Declarar um destruidor
virtual
é necessário apenas quando você planeja tornar suaclass
herança. Normalmente, as classes da biblioteca padrão (comostd::string
) não fornecem um destruidor virtual e, portanto, não são destinadas à subclassificação.fonte
delete
um ponteiro para uma classe base.Haverá uma sobrecarga no construtor para criar a vtable (se você não tiver outras funções virtuais, nesse caso, PROBABLY, mas nem sempre, também deverá ter um destruidor virtual). E se você não tiver outras funções virtuais, o objeto ficará com um tamanho de ponteiro maior do que o necessário. Obviamente, o tamanho aumentado pode ter um grande impacto em objetos pequenos.
Há uma leitura extra de memória para obter a tabela v e, em seguida, chamar a função indireta através disso, que é sobrecarga sobre o destruidor não virtual quando o destruidor é chamado. E, é claro, como conseqüência, um pequeno código extra gerado para cada chamada ao destruidor. Isso ocorre nos casos em que o compilador não pode deduzir o tipo real - naqueles casos em que pode deduzir o tipo real, o compilador não usará a vtable, mas chamará o destruidor diretamente.
Você deve ter um destruidor virtual se sua classe se destina a ser uma classe base, principalmente se ela pode ser criada / destruída por alguma outra entidade que não seja o código que sabe qual é o tipo na criação, então você precisa de um destruidor virtual.
Se você não tiver certeza, use o destruidor virtual. É mais fácil remover o virtual se ele aparecer como um problema do que tentar encontrar o bug causado por "o destruidor certo não é chamado".
Em resumo, você não deve ter um destruidor virtual se: 1. Você não possui nenhuma função virtual. 2. Não derive da classe (marque-a
final
em C ++ 11, assim o compilador dirá se você tentar derivar dela).Na maioria dos casos, criação e destruição não são grande parte do tempo gasto usando um objeto específico, a menos que exista "muito conteúdo" (criar uma sequência de 1 MB obviamente levará algum tempo, porque pelo menos 1 MB de dados precisa ser copiado de onde estiver localizado). Destruir uma sequência de 1 MB não é pior que a destruição de uma sequência de 150B; ambas exigirão a desalocação do armazenamento da sequência, e não muito mais; portanto, o tempo gasto lá normalmente é o mesmo [a menos que seja uma compilação de depuração, na qual a desalocação geralmente preenche a memória com um "padrão de envenenamento" - mas não é assim que você executará seu aplicativo real na produção].
Em resumo, existe uma pequena sobrecarga, mas para objetos pequenos, isso pode fazer a diferença.
Observe também que os compiladores podem otimizar a pesquisa virtual em alguns casos, por isso é apenas uma penalidade
Como sempre, quando se trata de desempenho, área ocupada por memória, e assim: Benchmark, perfil e medição, compare os resultados com alternativas e observe onde a maior parte do tempo / memória é gasta, e não tente otimizar os 90% de código que não é executado muito [a maioria dos aplicativos possui cerca de 10% de código altamente influente no tempo de execução e 90% de código que não tem muita influência]. Faça isso em um alto nível de otimização, para que você já tenha o benefício do compilador fazendo um bom trabalho! E repita, verifique novamente e melhore passo a passo. Não tente ser inteligente e tente descobrir o que é importante e o que não é, a menos que você tenha muita experiência com esse tipo específico de aplicativo.
fonte
You **should** have a virtual destructor if your class is intended as a base-class
é uma simplificação grosseira - e uma pessimização prematura . Isso só é necessário se alguém tiver permissão para excluir uma classe derivada através do ponteiro para a base. Em muitas situações, não é assim. Se você sabe que é, então, com certeza, suportar a sobrecarga. O qual, btw, é sempre adicionado, mesmo que as chamadas reais possam ser resolvidas estaticamente pelo compilador. Caso contrário, quando você controlar adequadamente o que as pessoas podem fazer com seus objetos, não vale a pena