Entendo a necessidade de um destruidor virtual. Mas por que precisamos de um destruidor virtual puro? Em um dos artigos em C ++, o autor mencionou que usamos destruidor virtual puro quando queremos tornar uma classe abstrata.
Mas podemos tornar uma classe abstrata tornando qualquer uma das funções-membro como pura virtual.
Então, minhas perguntas são
Quando realmente tornamos um destruidor virtual? Alguém pode dar um bom exemplo em tempo real?
Quando estamos criando classes abstratas, é uma boa prática tornar o destruidor também virtual? Se sim .. então porque?
c++
destructor
pure-virtual
Marca
fonte
fonte
Respostas:
Provavelmente, a verdadeira razão pela qual os destruidores virtuais puros são permitidos é que proibi-los significaria adicionar outra regra ao idioma e não há necessidade dessa regra, já que não há efeitos negativos ao permitir um destruidor virtual puro.
Não, virtual simples e antigo é suficiente.
Se você criar um objeto com implementações padrão para seus métodos virtuais e desejar torná-lo abstrato sem forçar alguém a substituir qualquer método específico , poderá tornar o destruidor virtual virtual. Não vejo muito sentido nisso, mas é possível.
Note-se que desde que o compilador irá gerar um destruidor implícita para classes derivadas, se o autor da classe não fazê-lo, quaisquer classes derivadas irá não ser abstrato. Portanto, ter o destruidor virtual puro na classe base não fará diferença para as classes derivadas. Isso tornará apenas a classe base abstrata (obrigado pelo comentário de @kappa ).
Pode-se também supor que toda classe derivada provavelmente precisaria ter um código de limpeza específico e usar o destruidor virtual puro como um lembrete para escrever um, mas isso parece artificial (e não forçado).
Nota: O destruidor é o único método que, mesmo que seja virtual puro, precisa ter uma implementação para instanciar classes derivadas (sim, funções virtuais puras podem ter implementações).
fonte
foof::bar
se você quiser ver por si mesmo.Tudo o que você precisa para uma classe abstrata é pelo menos uma função virtual pura. Qualquer função serve; mas, por acaso, o destruidor é algo que qualquer classe terá - por isso está sempre lá como candidato. Além disso, tornar o destruidor virtual puro (ao invés de virtual) não tem efeitos colaterais comportamentais além de tornar a classe abstrata. Como tal, muitos guias de estilo recomendam que o destruidor virtual puro seja usado de forma consistente para indicar que uma classe é abstrata - se por nenhum outro motivo, além de fornecer um local consistente, alguém que lê o código pode procurar para ver se a classe é abstrata.
fonte
Se você deseja criar uma classe base abstrata:
... é mais fácil tornar a classe abstrata, tornando o destruidor virtual e fornecendo uma definição (corpo do método) para ele.
Para o nosso hipotético ABC:
Você garante que ela não pode ser instanciada (mesmo interna da própria classe, é por isso que os construtores privados podem não ser suficientes), obtém o comportamento virtual desejado para o destruidor e não precisa encontrar e marcar outro método que não precisa de despacho virtual como "virtual".
fonte
Das respostas que li à sua pergunta, não pude deduzir um bom motivo para realmente usar um destruidor virtual puro. Por exemplo, o seguinte motivo não me convence:
Na minha opinião, destruidores virtuais puros podem ser úteis. Por exemplo, suponha que você tenha duas classes myClassA e myClassB no seu código e que myClassB herda de myClassA. Pelas razões mencionadas por Scott Meyers em seu livro "More Effective C ++", item 33 "Tornando as classes não folhas abstratas", é uma prática melhor criar uma classe abstrata myAbstractClass da qual myClassA e myClassB herdam. Isso fornece uma melhor abstração e evita alguns problemas que surgem com, por exemplo, cópias de objetos.
No processo de abstração (da criação da classe myAbstractClass), pode ser que nenhum método de myClassA ou myClassB seja um bom candidato por ser um método virtual puro (que é um pré-requisito para que myAbstractClass seja abstrato). Nesse caso, você define o destruidor da classe abstrata virtual puro.
A seguir, um exemplo concreto de algum código que eu mesmo escrevi. Eu tenho duas classes, Numerics / PhysicsParams, que compartilham propriedades comuns. Portanto, eu os deixo herdar da classe abstrata IParams. Nesse caso, eu não tinha absolutamente nenhum método em mãos que pudesse ser puramente virtual. O método setParameter, por exemplo, deve ter o mesmo corpo para cada subclasse. A única opção que tive foi tornar virtual o destruidor do IParams.
fonte
IParam
está protegido, como foi observado em outro comentário.Se você deseja interromper a instanciação da classe base sem fazer nenhuma alteração em sua classe derivada já implementada e testada, implemente um destruidor virtual puro em sua classe base.
fonte
Aqui eu quero dizer quando precisamos de destruidor virtual e quando precisamos de destruidor virtual puro
Quando você desejar que ninguém possa criar o objeto da classe Base diretamente, use destruidor virtual puro
virtual ~Base() = 0
. Normalmente, pelo menos uma função virtual pura é necessária, vamos assumirvirtual ~Base() = 0
, como essa função.Quando você não precisa da coisa acima, apenas precisa da destruição segura do objeto de classe Derived
Base * pBase = new Derivado (); excluir pBase; destruidor virtual puro não é necessário, apenas o destruidor virtual fará o trabalho.
fonte
Você está entrando em hipóteses com essas respostas, então tentarei fazer uma explicação mais simples e mais realista por uma questão de clareza.
Os relacionamentos básicos do design orientado a objetos são dois: IS-A e HAS-A. Eu não inventei isso. É assim que eles são chamados.
IS-A indica que um objeto específico se identifica como sendo da classe que está acima dele em uma hierarquia de classes. Um objeto de banana é um objeto de fruta se for uma subclasse da classe de fruta. Isso significa que em qualquer lugar que uma classe de frutas possa ser usada, uma banana pode ser usada. Não é reflexivo, no entanto. Você não pode substituir uma classe base por uma classe específica se essa classe específica for solicitada.
Has-a indicou que um objeto faz parte de uma classe composta e que existe um relacionamento de propriedade. Significa em C ++ que é um objeto membro e, como tal, o ônus recai sobre a classe proprietária para descartá-lo ou transferir a propriedade antes de se destruir.
Esses dois conceitos são mais fáceis de entender em linguagens de herança única do que em um modelo de herança múltipla como c ++, mas as regras são essencialmente as mesmas. A complicação ocorre quando a identidade da classe é ambígua, como passar um ponteiro de classe Banana para uma função que leva um ponteiro de classe Fruit.
As funções virtuais são, em primeiro lugar, uma coisa em tempo de execução. Faz parte do polimorfismo, pois é usado para decidir qual função executar no momento em que é chamada no programa em execução.
A palavra-chave virtual é uma diretiva de compilador para vincular funções em uma determinada ordem, se houver ambiguidade sobre a identidade da classe. As funções virtuais estão sempre nas classes pai (tanto quanto eu sei) e indicam ao compilador que a ligação das funções membro aos seus nomes deve ocorrer com a função subclasse primeiro e a função classe pai depois.
Uma classe Fruit pode ter uma função virtual color () que retorna "NONE" por padrão. A função color class () da classe Banana retorna "AMARELO" ou "MARROM".
Mas se a função que usa um ponteiro Fruit chama color () na classe Banana enviada para ele - qual função color () é chamada? A função normalmente chamaria Fruit :: color () para um objeto Fruit.
Isso não seria 99% do tempo pretendido. Mas se Fruit :: color () fosse declarado virtual, Banana: color () seria chamada para o objeto, porque a função color () correta seria vinculada ao ponteiro Fruit no momento da chamada. O tempo de execução verificará para qual objeto o ponteiro aponta, porque foi marcado como virtual na definição da classe Fruit.
Isso é diferente de substituir uma função em uma subclasse. Nesse caso, o ponteiro Fruit chamará Fruit :: color () se tudo o que souber é que ele é um ponteiro para Fruit.
Então agora surge a idéia de uma "função virtual pura". É uma frase bastante infeliz, pois a pureza não tem nada a ver com isso. Isso significa que se pretende que o método da classe base nunca seja chamado. Na verdade, uma função virtual pura não pode ser chamada. Ainda deve ser definido, no entanto. Uma assinatura de função deve existir. Muitos codificadores fazem uma implementação vazia {} para garantir a integridade, mas o compilador gerará uma internamente, se não. Nesse caso, quando a função é chamada, mesmo que o ponteiro seja para Fruit, Banana :: color () será chamada, pois é a única implementação de color () que existe.
Agora a peça final do quebra-cabeça: construtores e destruidores.
Construtores virtuais puros são ilegais, completamente. Isso acabou de sair.
Mas destruidores virtuais puros funcionam no caso em que você deseja proibir a criação de uma instância de classe base. Somente subclasses podem ser instanciadas se o destruidor da classe base for puro virtual. a convenção é atribuí-lo a 0.
Você precisa criar uma implementação neste caso. O compilador sabe que é isso que você está fazendo e garante que você faça o que é certo, ou queixa-se poderosamente de que não pode vincular a todas as funções necessárias para compilar. Os erros podem ser confusos se você não estiver no caminho certo sobre como está modelando sua hierarquia de classes.
Portanto, neste caso, você é proibido de criar instâncias de Fruit, mas tem permissão para criar instâncias de Banana.
Uma chamada para excluir o ponteiro Fruit que aponta para uma instância de Banana chama Banana :: ~ Banana () primeiro e depois chama Fuit :: ~ Fruit (), sempre. Porque não importa o que, quando você chama um destruidor de subclasse, o destruidor da classe base deve seguir.
É um modelo ruim? É mais complicado na fase de design, sim, mas pode garantir que a vinculação correta seja executada em tempo de execução e que uma função de subclasse seja executada onde houver ambiguidade quanto a exatamente qual subclasse está sendo acessada.
Se você escreve C ++ para passar apenas ponteiros de classe exatos sem ponteiros genéricos nem ambíguos, as funções virtuais não são realmente necessárias. Porém, se você precisar de flexibilidade de tipos de tempo de execução (como em Apple Banana Orange ==> Frutas), as funções se tornarão mais fáceis e versáteis com código menos redundante. Você não precisa mais escrever uma função para cada tipo de fruta e sabe que todas as frutas responderão a color () com sua própria função correta.
Espero que essa explicação extenuante solidifique o conceito em vez de confundir as coisas. Existem muitos bons exemplos por aí, e o suficiente e, na verdade, executá-los e mexer com eles, e você conseguirá.
fonte
Este é um tópico de uma década :) Leia os últimos 5 parágrafos do Item # 7 do livro "Effective C ++" para obter detalhes, começa em "Ocasionalmente, pode ser conveniente dar a uma classe um destruidor virtual puro ..."
fonte
Você pediu um exemplo e acredito que o seguinte fornece um motivo para um destruidor virtual puro. Estou ansioso para responder se este é um bom razão ...
Eu não quero que ninguém seja capaz de jogar o
error_base
tipo, mas os tipos de exceçãoerror_oh_shucks
eerror_oh_blast
têm funcionalidade idêntica e eu não quero escrevê-lo duas vezes. A complexidade do pImpl é necessária para evitar a exposiçãostd::string
a meus clientes e o uso destd::auto_ptr
exige o construtor de cópias.O cabeçalho público contém as especificações de exceção que estarão disponíveis para o cliente para distinguir os diferentes tipos de exceção lançados pela minha biblioteca:
E aqui está a implementação compartilhada:
A classe exception_string, mantida em sigilo, oculta std :: string da minha interface pública:
Meu código gera um erro como:
O uso de um modelo para
error
é um pouco gratuito. Ele economiza um pouco de código às custas de exigir que os clientes detectem erros como:fonte
Talvez exista outro CASO DE USO REAL do destruidor virtual puro que eu realmente não consigo ver em outras respostas :)
No começo, concordo plenamente com a resposta marcada: é porque proibir o destruidor virtual puro precisaria de uma regra extra na especificação da linguagem. Mas ainda não é o caso de uso que Mark está pedindo :)
Primeiro imagine isso:
e algo como:
Simplesmente - temos interface
Printable
e algum "contêiner" contendo qualquer coisa com essa interface. Eu acho que aqui está bem claro o porquêprint()
método é puro virtual. Pode ter algum corpo, mas, caso não exista uma implementação padrão, o virtual puro é uma "implementação" ideal (= "deve ser fornecida por uma classe descendente").E agora imagine exatamente o mesmo, exceto que não é para impressão, mas para destruição:
E também pode haver um contêiner semelhante:
É um caso de uso simplificado do meu aplicativo real. A única diferença aqui é que o método "especial" (destruidor) foi usado em vez do "normal"
print()
. Mas o motivo pelo qual é virtual puro ainda é o mesmo - não há código padrão para o método. Um pouco confuso pode ser o fato de que DEVE haver algum destruidor efetivamente e o compilador realmente gera um código vazio para ele. Mas, da perspectiva de um programador, pura virtualidade ainda significa: "Não tenho código padrão, ele deve ser fornecido por classes derivadas".Eu acho que não há nenhuma grande idéia aqui, apenas mais uma explicação de que a virtualidade pura funciona realmente de maneira uniforme - também para destruidores.
fonte
1) Quando você deseja exigir que as classes derivadas façam a limpeza. Isso é raro.
2) Não, mas você deseja que ele seja virtual.
fonte
precisamos tornar o destruidor virtual porque, se não o tornarmos virtual, o compilador destruirá apenas o conteúdo da classe base, n todas as classes derivadas permanecerão inalteradas, o compilador bacuse não chamará o destruidor de nenhuma outra classe, exceto a classe base.
fonte
delete
um ponteiro para a classe base quando, na verdade, ele aponta para sua derivada.