Quando NÃO usar destruidores virtuais?


48

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?


6
E se sua classe não for herdada? Olhe para muitas das classes de bibliotecas padrão, poucas possuem funções virtuais porque não foram projetadas para serem herdadas.
Algum programador

4
Também acho que, na maioria dos casos, os destruidores precisam ser virtuais. Não. De modo nenhum. Somente aqueles que abusam da herança (em vez de favorecer a composição) pensam assim. Vi aplicativos inteiros com apenas algumas classes básicas e funções virtuais.
Matthieu M.

11
@underscore_d Com implementações típicas, seria gerado código extra para qualquer classe polimórfica, a menos que todas essas coisas implícitas fossem desirtualizadas e otimizadas. Nas ABIs comuns, isso envolve pelo menos uma tabela para cada classe. O layout da classe também precisa ser alterado. Você não pode voltar com segurança depois de publicar uma classe como parte de alguma interface pública, porque alterá-la novamente quebraria a compatibilidade com a ABI, pois é obviamente ruim (se possível) esperar a desirtualização como contratos de interface em geral.
27418 FrankHB

11
@underscore_d A frase "em tempo de compilação" é imprecisa, mas acho que isso significa que um destruidor virtual não pode ser trivial nem com constexpr especificado, portanto é difícil evitar a geração extra de código (a menos que você evite totalmente a destruição de tais objetos), portanto prejudicaria mais ou menos o desempenho do tempo de execução.
FrankHB

2
@underscore_d O "ponteiro" parece um arenque vermelho. Possivelmente, deve ser um ponteiro para o membro (que não é um ponteiro por definição). Com ABIs comuns, um ponteiro para membro geralmente não se encaixa em uma palavra de máquina (como ponteiros típicos), e alterar uma classe de não-polimórfico para polimórfico frequentemente altera o tamanho do ponteiro para membro dessa classe.
27418 FrankHB

Respostas:


41

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 colocar newessa 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.

"na maioria dos casos, os destruidores precisam ser virtuais"

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.


23

Nesse caso, NÃO devo usar destruidores virtuais?

  1. Para uma classe concreta que não deseja ser herdada.
  2. Para uma classe base sem exclusão polimórfica. Um dos clientes não deve ser capaz de excluir polimorficamente usando um ponteiro para Base.

BTW,

Em qual caso deve usar destruidores virtuais?

Para classes base com exclusão polimórfica.


7
+1 para # 2, especificamente sem exclusão polimórfica . Se o seu destruidor nunca puder ser chamado por meio de um ponteiro base, torná-lo virtual é desnecessário e redundante, especialmente se sua classe não era virtual antes (por isso, torna-se inchado com o RTTI). Para se proteger contra qualquer usuário que viole isso, como Herb Sutter aconselhou, você tornaria o dtor da classe base protegido e não virtual, para que ele só possa ser invocado por / após um destruidor derivado.
underscore_d

imho @underscore_d que um ponto importante que eu perdi nas respostas, como na presença de herança o único caso em que eu não preciso de um construtor virtual é quando eu posso ter certeza de que nunca é necessário
formerlyknownas_463035818

14

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 Integerclasse, 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 vtablememó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 vptrarmazenado 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 vtablelado 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 objectclasse 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 Integertambém tende a exigir 16 bytes de memória em plataformas de 64 bits como resultado desses vptrmetadados de estilo associados por instância, e normalmente é impossível no Java agrupar algo como um único intem 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::vectorevita 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).


9

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.


Destruidores não deve utilizado em primeiro lugar em sistemas de tempo real rígidos já que muitos recursos como memória dinâmica não pode ser usado para fornecer fortes garantias de prazo
Marco A.

9
@MarcoA. Desde quando destruidores implicam alocação dinâmica de memória?
chbaker0

@ chbaker0 Eu usei um 'like'. Eles simplesmente não são usados ​​na minha experiência.
Marco A.

6
Também é absurdo que a memória dinâmica não possa ser usada em sistemas difíceis em tempo real. É bastante trivial provar que um heap pré-configurado com tamanhos de alocação fixos e um bitmap de alocação alocará memória ou retornará uma condição de falta de memória no tempo necessário para varrer esse bitmap.
MSalters

@msalters que me faz pensar: imagine um programa onde o custo de cada operação foi armazenado no sistema de tipos. Permitindo verificações em tempo real de garantias em tempo real.
Yakk

5

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:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

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.


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

3

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:

  • Se instâncias de classes derivadas não estiverem alocadas no heap, por exemplo, apenas diretamente na pilha ou dentro de outros objetos. (Exceto se você usar memória não inicializada e operador de posicionamento novo.)
  • Se instâncias de classes derivadas são alocadas no heap, mas a exclusão ocorre apenas por meio de ponteiros para a classe mais derivada, por exemplo, existe um 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 usando std::make_shared<Derived>(). É bom usar std::shared_ptr<Base>, contanto que o ponteiro inicial fosse a std::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 finalem 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.


1

Declarar um destruidor virtualé necessário apenas quando você planeja tornar sua classherança. Normalmente, as classes da biblioteca padrão (como std::string) não fornecem um destruidor virtual e, portanto, não são destinadas à subclassificação.


3
A razão é a subclasse + uso de polimorfismo. Um destruidor virtual é necessário apenas se uma resolução dinâmica for necessária, ou seja, uma referência / ponteiro / o que quer que seja que a classe principal possa realmente se referir a uma instância de uma subclasse.
Michel Billaud

2
@MichelBillaud, na verdade, você ainda pode ter polimorfismo sem dtors virtuais. Um dtor virtual é necessário SOMENTE para exclusão polimórfica, ou seja, chamar deleteum ponteiro para uma classe base.
chbaker0

1

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


11
"será uma sobrecarga no construtor para a criação da vtable" - a vtable é normalmente "criada" por classe pelo compilador, com o construtor apenas tendo a sobrecarga de armazenar um ponteiro para ele na instância do objeto em construção.
21415 Tony

Além disso ... eu evito a otimização prematura, mas, inversamente, 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
underscore_d
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.