Por que só a herança não basta
Na página anterior você construiu uma hierarquia de classes: uma classe derivada herda os membros da sua base. Mas há um detalhe. Quando você chama um método por meio de um ponteiro da classe base, o C++ decide qual função executar com base no tipo do ponteiro, e não no tipo real do objeto. Assim, um Animal* que na verdade aponta para um Dog ainda chama a versão do método de Animal.
Isso quase nunca é o que você quer. Na maioria das vezes você tem uma coleção de ponteiros da classe base, cada um apontando para um objeto derivado diferente, e quer que cada um se comporte como ele mesmo. As funções virtuais fazem isso acontecer.
O objeto é genuinamente um Dog, mas ainda assim a->speak() executou Animal::speak(). Como speak não é virtual, o compilador escolheu a função em tempo de compilação a partir do tipo estático Animal*. Este é o bug que as funções virtuais existem para corrigir.
Tornando uma função virtual
Adicione a palavra-chave virtual ao método da classe base. Agora a chamada é resolvida em tempo de execução com base no tipo real do objeto - isto é o dynamic dispatch.
Um único laço sobre Animal*, três comportamentos diferentes. O ponteiro da base "sabe" o tipo real em tempo de execução e despacha de acordo. Esse único mecanismo - uma interface, muitas implementações - é o que significa polimorfismo em C++.
Observe que virtual só precisa aparecer na declaração da base; uma vez que uma função é virtual, ela permanece virtual automaticamente em toda classe derivada. Escrevê-la de novo na classe derivada é opcional e redundante.
Sempre use a palavra-chave override
No exemplo acima, cada método derivado está marcado com override. É opcional para o código funcionar, mas você deve tratá-lo como obrigatório. override (C++11) pede ao compilador que verifique se você realmente está sobrescrevendo uma função virtual da base com uma assinatura correspondente. Se você errar a assinatura sutilmente, recebe um erro claro em vez de um bug silencioso.
struct Animal {
virtual void speak() const { } // observe: const
};
struct Dog : Animal {
void speak() { } // NÃO é const - isto é uma função NOVA, não uma sobrescrita!
void speak() override { } // erro: 'speak' não sobrescreve - avisa você imediatamente
};
Sem override, o primeiro speak() compila sem problemas, mas nunca é chamado por meio de um Animal*, porque sua assinatura difere da base (falta o const). Você passaria uma tarde inteira se perguntando por que sua sobrescrita não faz nada. Com override, o compilador detecta a incompatibilidade na hora. Adicione-o a toda função que sobrescreve.
Funções virtuais puras e classes abstratas
Às vezes a classe base não tem um padrão sensato - que som faz um "Animal" genérico? Nesse caso, declare a função como virtual pura atribuindo = 0. Isso a deixa sem corpo e transforma a classe em uma classe abstrata que não pode ser instanciada por si só. Ela existe apenas para definir uma interface que as classes derivadas devem cumprir.
Toda subclasse concreta precisa implementar area(), ou também permanece abstrata. É assim que o C++ expressa "interfaces": uma classe abstrata só com funções virtuais puras é o equivalente em C++ a uma interface em linguagens como Java.
A regra do destrutor virtual
Esta é a armadilha que pega todo mundo pelo menos uma vez. Quando você faz delete em um objeto por meio de um ponteiro da classe base, o C++ chama o destrutor que encontra - e se esse destrutor não for virtual, ele só executa o destrutor da base. A parte derivada nunca é destruída, vazando tudo o que ela possuía. O padrão chama isso de comportamento indefinido.
A correção é uma única palavra: torne o destrutor da base virtual. Então delete p executa ~Derived primeiro e depois ~Base, exatamente como deveria.
struct Base {
virtual ~Base() { cout << "~Base\n"; } // correto
};
// agora: ~Derived e depois ~Base
Regra prática: no momento em que uma classe tiver qualquer função virtual, dê a ela também um destrutor virtual. Se uma classe é destinada a ser uma classe base usada por meio de ponteiros, seu destrutor precisa ser virtual.
Erros comuns e armadilhas
Mais algumas armadilhas para ficar de olho quando você já estiver à vontade com funções virtuais:
Object slicing (fatiamento de objetos). Se você passar ou armazenar um objeto derivado por valor em uma variável da base, a parte derivada é "fatiada" e você fica com um objeto base simples - o despacho virtual não alcança mais a sobrescrita. Sempre use ponteiros ou referências para polimorfismo:
Dog d;
Animal a = d; // FATIADO: a agora é só um Animal, a parte Dog se foi
a.speak(); // executa Animal::speak mesmo sendo virtual
Animal& ref = d; // OK: a referência mantém o tipo real
ref.speak(); // executa Dog::speak
Não chame funções virtuais de construtores ou destrutores. Durante a construção, a parte derivada ainda não existe, então uma chamada virtual resolve para a versão da classe atual, não para a sobrescrita derivada - raramente o que você pretende.
O despacho virtual tem um pequeno custo. Cada chamada virtual passa por uma tabela oculta de ponteiros de função (a "vtable"), uma indireção por chamada. É barato, mas não é grátis, então não torne uma função virtual a menos que você realmente precise de sobrescrita.
Chamar a versão da base de propósito. Dentro de uma sobrescrita você ainda pode invocar a implementação da base explicitamente com Base::method() - útil quando o comportamento derivado estende em vez de substituir o da base.
Próximo: Sobrecarga de operadores
As funções virtuais permitem que seus objetos personalizem seu comportamento por meio de uma interface compartilhada. A próxima página mostra como personalizar os operadores que agem sobre seus objetos: com a sobrecarga de operadores você pode ensinar seus próprios tipos a responder a +, ==, << e mais, de modo que Vector + Vector ou cout << myObject se leiam de forma tão natural quanto para os tipos nativos.
Perguntas frequentes
O que é uma função virtual em C++?
Uma função virtual é uma função membro declarada com a palavra-chave virtual em uma classe base, de modo que, quando você a chama por meio de um ponteiro ou referência da classe base, o C++ executa a sobrescrita da classe derivada em vez da versão da base. Essa seleção em tempo de execução é chamada de dynamic dispatch (despacho dinâmico) e é a base do polimorfismo.
Qual é a diferença entre uma função virtual e uma função virtual pura?
Uma função virtual tem corpo e pode ser sobrescrita. Uma função virtual pura é declarada com = 0 e não tem corpo na classe base - ela obriga toda classe derivada concreta a fornecer uma implementação. Qualquer classe com pelo menos uma função virtual pura é uma classe abstrata e não pode ser instanciada.
Por que uma classe base precisa de um destrutor virtual em C++?
Se você fizer delete em um objeto derivado por meio de um ponteiro da classe base e o destrutor da base não for virtual, apenas o destrutor da base é executado - a parte derivada nunca é liberada, o que vaza recursos e é comportamento indefinido. Declare como virtual o destrutor de qualquer classe destinada a ser usada de forma polimórfica.