Função virtual pura com implementação

176

Meu entendimento básico é que não há implementação para uma função virtual pura, no entanto, disseram-me que pode haver implementação para uma função virtual pura.

class A {
public:
    virtual void f() = 0;
};

void A::f() {
    cout<<"Test"<<endl;
}

O código está acima de OK?

Qual é o objetivo de torná-lo uma função virtual pura com uma implementação?

skydoor
fonte

Respostas:

215

Uma virtualfunção pura deve ser implementada em um tipo derivado que será instanciado diretamente, no entanto, o tipo base ainda pode definir uma implementação. Uma classe derivada pode chamar explicitamente a implementação da classe base (se as permissões de acesso permitirem) usando um nome com escopo completo (chamando A::f()no seu exemplo - se A::f()fosse publicou protected). Algo como:

class B : public A {

    virtual void f() {
        // class B doesn't have anything special to do for f()
        //  so we'll call A's

        // note that A's declaration of f() would have to be public 
        //  or protected to avoid a compile time problem

        A::f();
    }

};

O caso de uso em que consigo pensar é quando há um comportamento padrão mais ou menos razoável, mas o designer de classe deseja que esse tipo de comportamento seja invocado apenas explicitamente. Também pode ser o caso em que você deseja que as classes derivadas sempre executem seu próprio trabalho, mas também possam chamar um conjunto comum de funcionalidades.

Observe que, embora seja permitido pela linguagem, não é algo que eu vejo comumente usado (e o fato de que isso pode ser feito parece surpreender a maioria dos programadores de C ++, mesmo os mais experientes).

Michael Burr
fonte
1
Você esqueceu de adicionar por que surpreende os programadores: é porque a definição em linha é proibida por padrão. As definições de métodos virtuais puros devem ser deported. (em .inl ou .cpp para se referir a práticas comuns de nomeação de arquivos).
precisa saber é
portanto, este método de chamada é igual ao membro do método estático que chama. Algum tipo de método de classe em Java.
precisa saber é o seguinte
2
"não é comumente usado" == má prática? Eu estava procurando exatamente o mesmo comportamento, tentando implementar o NVI. E a NVI parece uma boa prática para mim.
Saskia
5
Vale ressaltar que tornar A :: f () puro significa que B deve implementar f () (caso contrário, B seria abstrato e instável). E como o @MichaelBurr aponta, fornecer uma implementação para A :: f () significa que B pode usá-lo para definir f ().
Fearless_fool
2
IIRC, Scot Meyer tem um excelente artigo sobre o caso de uso desta questão em uma de seu livro clássico "mais eficaz C ++"
irsis
75

Para ser claro, você está entendendo errado o que = 0; depois de uma função virtual significa.

= 0 significa que as classes derivadas devem fornecer uma implementação, não que a classe base não possa fornecer uma implementação.

Na prática, quando você marca uma função virtual como pura (= 0), há muito pouco sentido em fornecer uma definição, porque ela nunca será chamada, a menos que alguém o faça explicitamente via Base :: Function (...) ou se o parâmetro O construtor da classe base chama a função virtual em questão.

Terry Mahaffey
fonte
9
Isto está incorreto. Se você invocar essa função virtual pura no construtor de sua classe virtual pura, uma chamada virtual pura será feita. Nesse caso, é melhor você ter uma implementação.
Rmn
@ rmn, Sim, você está certo sobre chamadas virtuais em construtores. Eu atualizei a resposta. Felizmente, todo mundo sabe que não deve fazer isso. :)
Terry Mahaffey
3
De fato, fazer uma chamada pura de base de um construtor resulta em comportamento definido pela implementação. No VC ++, isso equivale a uma falha de _purecall.
Ofek Shilon
@OfekShilon está correto - eu ficaria tentado a chamá-lo de comportamento indefinido e candidato a más práticas / refatoração de código (ou seja, chamar métodos virtuais dentro do construtor). Eu acho que isso tem a ver com a coerência da tabela virtual, que pode não estar preparada para rotear para o corpo da implementação correta.
Teodron 18/05/19
1
Nos construtores e destruidores, as funções virtuais não são virtuais.
precisa
20

A vantagem disso é que ele força os tipos derivados a ainda substituir o método, mas também fornece uma implementação padrão ou aditiva.

JaredPar
fonte
1
Por que eu gostaria de forçar se há uma implementação padrão? Isso soa como funções virtuais normais. Se fosse apenas uma função virtual normal, eu posso substituir e, se não, a implementação padrão será fornecida (implementação da base).
StackExchange123 7/03
19

Se você possui um código que deve ser executado pela classe derivada, mas não deseja que seja executado diretamente - e deseja forçá-lo a ser substituído.

Seu código está correto, apesar de tudo isso não ser um recurso usado com frequência, e geralmente visto apenas ao tentar definir um destruidor virtual puro - nesse caso, você deve fornecer uma implementação. O engraçado é que, depois que você deriva dessa classe, não precisará substituir o destruidor.

Portanto, o uso sensato de funções virtuais puras é especificar um destruidor virtual puro como uma palavra-chave "não final".

O código a seguir está surpreendentemente correto:

class Base {
public:
  virtual ~Base() = 0;
};

Base::~Base() {}

class Derived : public Base {};

int main() { 
  // Base b; -- compile error
  Derived d; 
}
Kornel Kisielewicz
fonte
1
Os destruidores da classe base são sempre chamados de qualquer maneira, virtuais ou não e puros ou não; com outras funções, você não pode garantir que uma função virtual substituta chame a implementação da classe base, independentemente de a versão da classe base ser pura ou não.
CB Bailey
1
Esse código está errado. Você deve definir o dtor fora da definição de classe devido a uma peculiaridade de sintaxe do idioma.
@ Roger: obrigado, isso realmente me ajudou - este é o código que eu tenho usado, ele compila muito bem no MSVC, mas acho que não seria portátil.
Kornel Kisielewicz
4

Sim isto está correcto. No seu exemplo, as classes derivadas de A herdam a interface f () e uma implementação padrão. Mas você força classes derivadas a implementar o método f () (mesmo que seja apenas para chamar a implementação padrão fornecida por A).

Scott Meyers discute isso no item C ++ eficaz (2ª edição) # 36 Diferencie entre herança da interface e herança da implementação. O número do item pode ter sido alterado na última edição.

Yukiko
fonte
4

Funções virtuais puras com ou sem um corpo simplesmente significam que os tipos derivados devem fornecer sua própria implementação.

Os corpos puros de funções virtuais na classe base são úteis se suas classes derivadas quiserem chamar sua implementação de classe base.

Brian R. Bondy
fonte
2

O 'virtual void foo () = 0;' sintaxe não significa que você não pode implementar foo () na classe atual, você pode. Isso também não significa que você deve implementá-lo em classes derivadas . Antes de me dar um tapa, vamos observar o problema do diamante: (código implícito, veja bem).

class A
{
public: 
    virtual void foo()=0;
    virtual void bar();
}

class B : public virtual A
{
public:
    void foo() { bar(); }
}

class C : public virtual A
{
public:
    void bar();
}

class D : public B, public C
{}

int main(int argc, const char* argv[])
{
    A* obj = new D();
    **obj->foo();**
    return 0;
}

Agora, a invocação obj-> foo () resultará em B :: foo () e depois em C :: bar ().

Você vê ... métodos virtuais puros não precisam ser implementados em classes derivadas (foo () não tem implementação na classe C - o compilador compilará) No C ++, existem muitas brechas.

Espero poder ajudar :-)

Nir Hedvat
fonte
5
Ele não precisa ser implementado em TODAS as classes derivadas, mas DEVE ter uma implementação em todas as classes derivadas que você pretende instanciar. Você não pode instanciar um objeto do tipo Cno seu exemplo. Você pode instanciar um objeto do tipo Dporque ele obtém sua implementação de foofrom B.
precisa
0

Um caso de uso importante de ter um método virtual puro com um corpo de implementação é quando você deseja ter uma classe abstrata, mas não possui métodos adequados na classe para torná-la virtual. Nesse caso, você pode tornar o destruidor da classe virtual e colocar a implementação desejada (mesmo um corpo vazio) para isso. Como um exemplo:

class Foo
{
   virtual ~Foo() = 0;
   void bar1() {}
   void bar2(int x) {}
   // other methods
};

Foo::~Foo()
{
}

Essa técnica torna a Fooclasse abstrata e, como resultado, impossível instanciar a classe diretamente. Ao mesmo tempo, você não adicionou um método virtual puro adicional para tornar a Fooclasse abstrata.

Gupta
fonte