Por que precisamos de um destruidor virtual puro em C ++?

154

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

  1. Quando realmente tornamos um destruidor virtual? Alguém pode dar um bom exemplo em tempo real?

  2. Quando estamos criando classes abstratas, é uma boa prática tornar o destruidor também virtual? Se sim .. então porque?

Marca
fonte
14
@ Daniel- Os links mencionados não respondem à minha pergunta. Ele responde por que um destruidor virtual puro deve ter uma definição. Minha pergunta é por que precisamos de um destruidor virtual puro.
Mark Mark
Eu estava tentando descobrir o motivo, mas você já fez a pergunta aqui.
Nsivakr

Respostas:

119
  1. 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.

  2. 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).

struct foo {
    virtual void bar() = 0;
};

void foo::bar() { /* default implementation */ }

class foof : public foo {
    void bar() { foo::bar(); } // have to explicitly call default implementation.
};
Motti
fonte
13
"sim, funções virtuais puras podem ter implementações" Então não é virtual puro.
GManNickG 02/08/09
2
Se você deseja tornar uma classe abstrata, não seria mais simples apenas proteger todos os construtores?
bdonlan
78
@ GMan, você está enganado, sendo puro virtual significa que as classes derivadas devem substituir esse método, isso é ortogonal à implementação. Confira meu código e comente foof::barse você quiser ver por si mesmo.
Motti
15
@ GMan: a lista de perguntas frequentes do C ++ diz "Observe que é possível fornecer uma definição para uma função virtual pura, mas isso geralmente confunde os iniciantes e é melhor evitar até mais tarde". parashift.com/c++-faq-lite/abcs.html#faq-22.4 A Wikipedia (esse bastião da correção) também diz o mesmo. Acredito que o padrão ISO / IEC usa terminologia semelhante (infelizmente minha cópia está em funcionamento no momento) ... Concordo que é confuso e geralmente não uso o termo sem esclarecimentos quando for fornecer uma definição, especialmente programadores ao redor mais recentes ...
Leander
9
@ Motti: O que é interessante aqui e fornece mais confusão é que o destruidor virtual puro NÃO precisa ser explicitamente substituído pela classe derivada (e instanciada). Nesse caso, a definição implícita é usada :)
kappa
33

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.

Braden
fonte
1
mas ainda por que fornecer a implementação do destruidor de virtaul puro. O que poderia dar errado, eu faço um destruidor virtual puro e não fornece sua implementação. Presumo que apenas os ponteiros das classes base sejam declarados e, portanto, o destruidor da classe abstrata nunca é chamado.
precisa
4
@ Surf: porque um destruidor de uma classe derivada chama implicitamente o destruidor de sua classe base, mesmo que esse destruidor seja virtual puro. Portanto, se não houver implementação para isso, um comportamento indefinido vai acontecer.
precisa saber é o seguinte
19

Se você deseja criar uma classe base abstrata:

  • que não pode ser instanciado (sim, isso é redundante com o termo "abstrato"!)
  • mas precisa do comportamento do destruidor virtual (você pretende transportar ponteiros para o ABC em vez de ponteiros para os tipos derivados e excluí-los)
  • mas não precisa de nenhum outro comportamento de despacho virtual para outros métodos (talvez não existam outros métodos? considere um contêiner de "recurso" protegido simples que precise de construtores / destruidor / atribuição, mas não muito mais)

... é 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".

leander
fonte
8

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:

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.

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.

struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> m_Parameters;
};

struct NumericsParams : IParams
{
    NumericsParams(const ModelConfiguration& aNumericsConf);
    virtual ~NumericsParams();

    double dt() const;
    double ti() const;
    double tf() const;
};

struct PhysicsParams : IParams
{
    PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
    virtual ~PhysicsParams();

    double g()     const; 
    double rho_i() const; 
    double rho_w() const; 
};
Laurent Michel
fonte
1
Eu gosto desse uso, mas outra maneira de "impor" a herança é declarar que o construtor IParamestá protegido, como foi observado em outro comentário.
Rwols
4

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.

sukumar
fonte
3

Aqui eu quero dizer quando precisamos de destruidor virtual e quando precisamos de destruidor virtual puro

class Base
{
public:
    Base();
    virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly 
};

Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }


class Derived : public Base
{
public:
    Derived();
    ~Derived();
};

Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() {   cout << "Derived Destructor" << endl; }


int _tmain(int argc, _TCHAR* argv[])
{
    Base* pBase = new Derived();
    delete pBase;

    Base* pBase2 = new Base(); // Error 1   error C2259: 'Base' : cannot instantiate abstract class
}
  1. 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 assumir virtual ~Base() = 0, como essa função.

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

Anil8753
fonte
2

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.

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

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

Chris Reid
fonte
1

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

JQ
fonte
0

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_basetipo, mas os tipos de exceção error_oh_shuckse error_oh_blasttêm funcionalidade idêntica e eu não quero escrevê-lo duas vezes. A complexidade do pImpl é necessária para evitar a exposição std::stringa 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:

// error.h

#include <exception>
#include <memory>

class exception_string;

class error_base : public std::exception {
 public:
  error_base(const char* error_message);
  error_base(const error_base& other);
  virtual ~error_base() = 0; // Not directly usable

  virtual const char* what() const;
 private:
  std::auto_ptr<exception_string> error_message_;
};

template<class error_type>
class error : public error_base {
 public:
   error(const char* error_message) : error_base(error_message) {}
   error(const error& other) : error_base(other) {}
   ~error() {}
};

// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }

E aqui está a implementação compartilhada:

// error.cpp

#include "error.h"
#include "exception_string.h"

error_base::error_base(const char* error_message)
  : error_message_(new exception_string(error_message)) {}

error_base::error_base(const error_base& other)
  : error_message_(new exception_string(other.error_message_->get())) {}

error_base::~error_base() {}

const char* error_base::what() const {
  return error_message_->get();
}

A classe exception_string, mantida em sigilo, oculta std :: string da minha interface pública:

// exception_string.h

#include <string>

class exception_string {
 public:
  exception_string(const char* message) : message_(message) {}

  const char* get() const { return message_.c_str(); }
 private:
  std::string message_;
};

Meu código gera um erro como:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

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:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}
Rai
fonte
0

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:

class Printable {
  virtual void print() const = 0;
  // virtual destructor should be here, but not to confuse with another problem
};

e algo como:

class Printer {
  void queDocument(unique_ptr<Printable> doc);
  void printAll();
};

Simplesmente - temos interface Printablee 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:

class Destroyable {
  virtual ~Destroyable() = 0;
};

E também pode haver um contêiner semelhante:

class PostponedDestructor {
  // Queues an object to be destroyed later.
  void queObjectForDestruction(unique_ptr<Destroyable> obj);
  // Destroys all already queued objects.
  void destroyAll();
};

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

Jarek C
fonte
-2

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.

Steven Sudit
fonte
-2

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.

Asad hashmi
fonte
-1: A questão não é sobre por que um destruidor deve ser virtual.
Trovador
Além disso, em certas situações, os destruidores não precisam ser virtuais para alcançar a destruição correta. Destruidores virtuais são necessários apenas quando você acaba chamando deleteum ponteiro para a classe base quando, na verdade, ele aponta para sua derivada.
CygnusX1
Você está 100% correto. Essa é e foi no passado uma das fontes número um de vazamentos e falhas nos programas C ++, o terceiro apenas na tentativa de fazer coisas com ponteiros nulos e exceder os limites das matrizes. Um destruidor de classe base não virtual será chamado em um ponteiro genérico, ignorando o destruidor de subclasse inteiramente se não estiver marcado como virtual. Se houver algum objeto criado dinamicamente pertencente à subclasse, ele não será recuperado pelo destruidor de base em uma chamada para exclusão. Você está bebendo bem, então BLUURRK! (difícil de encontrar onde, também.)
Chris Reid