C++ Polimorfismo (virtual)
No fim da aula de Herança, uma coisa estranha aconteceu: o Livro tinha o seu próprio mostrar(), mas, passado para uma função que recebia const Produto&, rodou o mostrar() do Produto. Nesta aula você vai entender por quê e consertar com uma palavra: virtual. É ela que permite guardar cachorros, gatos e peixes na mesma lista e mandar "todo mundo falar", cada um do seu jeito. Esse "cada um do seu jeito" se chama polimorfismo, do grego "muitas formas".
- O problema: sem
virtual, uma referência da base chama o método da base. virtualna base eoverridena filha, e o que cada um faz.- Como chamar pela referência (
Animal&) ou pelo ponteiro (Animal*) da base. - Como o
overridetransforma um engano silencioso num erro de compilação. - Por que a classe base precisa de destrutor
virtual, e o aviso que o g++ dá. - Como guardar objetos diferentes num
std::vector<std::unique_ptr<Animal>>. - A armadilha do fatiamento e o
finalnum método.
Os programas foram compilados com o g++ 15 na máquina da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: "todo mundo, falem!"
A Ana abriu um pet shop. De manhã, ela passa pelas gaiolas e diz "bom dia!". Ela não precisa saber, gaiola por gaiola, que tipo de bicho está ali: o cachorro late, o gato mia, o peixe fica quieto. A mesma ordem produz respostas diferentes, conforme quem recebe.
Em código: a Ana quer uma lista de Animal e um laço que chama falar() em cada um. Quem decide qual falar() roda deve ser o bicho de verdade, não o tipo da lista. Vamos ver que isso não acontece sozinho.
1. O problema: sem virtual
#include <iostream>
#include <string>
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
std::string nome() const { return nome_; }
void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal{nome} {}
void falar() const { std::cout << nome() << ": Au au!\n"; }
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() const { std::cout << nome() << ": Miau!\n"; }
};
void apresentar(const Animal& a) {
a.falar();
}
int main() {
Cachorro rex{"Rex"};
Gato mimi{"Mimi"};
rex.falar();
mimi.falar();
std::cout << "-- pela função apresentar --\n";
apresentar(rex);
apresentar(mimi);
std::cout << "-- por um ponteiro de Animal --\n";
Animal* p = &rex;
p->falar();
}
$ g++ -std=c++23 -Wall -Wextra semvirtual.cpp -o semvirtual $ ./semvirtual Rex: Au au! Mimi: Miau! -- pela função apresentar -- Rex: ... « o Rex é um Cachorro, mas rodou o falar() do Animal Mimi: ... -- por um ponteiro de Animal -- Rex: ...
Chamando direto no objeto (rex.falar()), tudo certo. Mas, pela referência const Animal& ou pelo ponteiro Animal*, rodou o falar() do Animal. O motivo: sem virtual, o compilador escolhe o método na hora de compilar, olhando só o tipo escrito no código. Dentro de apresentar, está escrito Animal; o compilador não sabe (e não pergunta) se, na hora de rodar, vai chegar um cachorro ou um gato.
| Nome | O que é | Em apresentar(rex) |
|---|---|---|
| Tipo estático | O tipo escrito no código | Animal (o parâmetro é const Animal&) |
| Tipo dinâmico | O tipo do objeto de verdade, na hora de rodar | Cachorro |
Sem virtual, manda o tipo estático. Com virtual, manda o tipo dinâmico.
2. A solução: virtual e override
Duas palavras, em três linhas:
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
std::string nome() const { return nome_; }
virtual void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Au au!\n"; }
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Miau!\n"; }
};
// apresentar e main iguais ao programa anterior
- virtual
- Na base, antes do tipo de retorno. Diz: "as classes filhas podem trocar este método, e quem chamar por uma referência ou ponteiro de
Animaldeve receber a versão do objeto de verdade". Um método assim se chama função virtual. - void falar() const
- Na filha, a assinatura precisa ser idêntica à da base: mesmo nome, mesmos parâmetros, mesmo
const. - override
- Na filha, depois dos parênteses (e do
const). Quer dizer "sobrescreve": "este método substitui umvirtualda base". É opcional, mas, como você vai ver na seção 3, deveria ser obrigatório na sua cabeça. C++11
$ g++ -std=c++23 -Wall -Wextra comvirtual.cpp -o comvirtual $ ./comvirtual Rex: Au au! Mimi: Miau! -- pela função apresentar -- Rex: Au au! « agora quem decide é o objeto de verdade Mimi: Miau! -- por um ponteiro de Animal -- Rex: Au au!
A função apresentar não mudou nada e continua sem saber que cachorros e gatos existem. Mesmo assim, cada animal falou do seu jeito. Isso é o polimorfismo: um código escrito para a base funciona com todas as filhas, inclusive as que forem criadas no ano que vem.
Toda classe com função virtual ganha uma tabela escondida, a vtable (tabela virtual), com o endereço da versão certa de cada método virtual. Cada objeto carrega um ponteiro escondido para a tabela da sua classe. Na chamada a.falar(), o programa olha a tabela do objeto e pula para o endereço que está lá. O custo é pequeno: um ponteiro a mais por objeto e uma consulta por chamada.
| Jeito de chamar | Sem virtual | Com virtual |
|---|---|---|
rex.falar() (o próprio objeto) | Cachorro | Cachorro |
Animal& r = rex; r.falar() | Animal | Cachorro |
Animal* p = &rex; p->falar() | Animal | Cachorro |
Animal copia = rex; copia.falar() | Animal | Animal (veja fatiamento) |
Experimente: rode e confira que cada animal fala do seu jeito. Depois apague a palavra virtual do Animal (o compilador vai reclamar dos override: apague-os também) e rode de novo. Por fim, crie uma classe Vaca que diz Muuu!.
#include <iostream>
#include <string>
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
std::string nome() const { return nome_; }
virtual void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Au au!\n"; }
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Miau!\n"; }
};
void apresentar(const Animal& a) {
a.falar();
}
int main() {
Cachorro rex{"Rex"};
Gato mimi{"Mimi"};
apresentar(rex);
apresentar(mimi);
}
3. override: o compilador conferindo por você
Para substituir um método virtual, a assinatura da filha tem que ser exatamente igual à da base. Um detalhe diferente (um const esquecido, um int no lugar de double) e o método da filha vira um método novo, que não substitui nada. Veja o Gato sem o const, e sem override:
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
virtual ~Animal() = default;
std::string nome() const { return nome_; }
virtual void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() { std::cout << nome() << ": Miau!\n"; } // faltou o const
};
int main() {
Gato mimi{"Mimi"};
const Animal& a = mimi;
a.falar();
}
$ g++ -std=c++23 -Wall -Wextra semoverride.cpp -o semoverride
semoverride.cpp:9:18: warning: ‘virtual void Animal::falar() const’ was hidden [-Woverloaded-virtual=]
9 | virtual void falar() const { std::cout << nome_ << ": ...\n"; }
| ^~~~~
semoverride.cpp:17:10: note: by ‘void Gato::falar()’
17 | void falar() { std::cout << nome() << ": Miau!\n"; }
| ^~~~~
$ ./semoverride
Mimi: ... « a gata ficou muda
Compilou, rodou, e a resposta está errada. O g++ 15, com -Wall, até deu um aviso ("o virtual Animal::falar() const foi escondido por Gato::falar()"), mas aviso é fácil de ignorar no meio de outros. Agora, a mesma coisa com override:
void falar() override { std::cout << nome() << ": Miau!\n"; } // faltou o const
$ g++ -std=c++23 -Wall -Wextra override.cpp -o override
override.cpp:17:10: error: ‘void Gato::falar()’ marked ‘override’, but does not override
17 | void falar() override { std::cout << nome() << ": Miau!\n"; }
| ^~~~~
override.cpp:9:18: warning: ‘virtual void Animal::falar() const’ was hidden [-Woverloaded-virtual=]
9 | virtual void falar() const { std::cout << nome_ << ": ...\n"; }
| ^~~~~
override.cpp:17:10: note: by ‘void Gato::falar()’
17 | void falar() override { std::cout << nome() << ": Miau!\n"; }
| ^~~~~
- error:
- Agora é erro: o programa nem é gerado.
- ‘void Gato::falar()’ marked ‘override’, but does not override
- "
Gato::falar()está marcado comooverride, mas não sobrescreve nada". Você prometeu substituir um método da base, e não existe nenhum método virtual da base com essa assinatura. - warning: … was hidden / note: by …
- A pista: compare as duas linhas. A da base tem
const; a doGato, não.
O Clang do editor diz non-virtual member function marked 'override' hides virtual member function. Conserto: void falar() const override. Moral: escreva override em todo método que substitui um virtual. Custa uma palavra e transforma um defeito silencioso em erro com número de linha.
4. Destrutor virtual
O destrutor também é um método, e a mesma regra vale para ele. Se você destrói um Cachorro por um ponteiro de Animal, e o destrutor do Animal não é virtual, só a parte Animal é destruída:
#include <iostream>
#include <string>
class Animal {
public:
virtual void falar() const { std::cout << "...\n"; }
~Animal() { std::cout << "~Animal\n"; }
};
class Cachorro : public Animal {
public:
Cachorro() : brinquedos_{new std::string[3]} {}
void falar() const override { std::cout << "Au au!\n"; }
~Cachorro() {
delete[] brinquedos_;
std::cout << "~Cachorro (devolvi os brinquedos)\n";
}
private:
std::string* brinquedos_;
};
int main() {
Animal* p = new Cachorro;
p->falar();
delete p;
}
$ g++ -std=c++23 -Wall -Wextra destrutor.cpp -o destrutor
destrutor.cpp: In function ‘int main()’:
destrutor.cpp:25:5: warning: deleting object of polymorphic class type ‘Animal’ which has non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor]
25 | delete p;
| ^~~~~~~~
$ ./destrutor
Au au!
~Animal « o ~Cachorro nunca rodou: os brinquedos vazaram
O aviso diz: "apagar um objeto de classe polimórfica Animal, que tem destrutor não virtual, pode causar comportamento indefinido". E a saída confirma: o ~Cachorro não rodou, e o array de brinquedos nunca foi devolvido. (O Clang avisa com delete called on non-final 'Animal' that has virtual functions but non-virtual destructor.)
O conserto é uma palavra: virtual ~Animal() { … }.
$ g++ -std=c++23 -Wall -Wextra destrutor2.cpp -o destrutor2 $ ./destrutor2 Au au! ~Cachorro (devolvi os brinquedos) « primeiro a filha... ~Animal « ...depois a base, como na aula de Herança
Se uma classe tem qualquer método virtual, dê a ela um destrutor virtual. Se ela não precisa fazer nada no destrutor, escreva virtual ~Animal() = default;: "a versão padrão, só que virtual". Isso vale também para std::unique_ptr<Animal>, que faz o mesmo delete por um ponteiro de Animal.
5. Uma lista de animais diferentes: vector<unique_ptr<Animal>>
Agora o objetivo da Ana: todos os bichos numa lista só. Um std::vector<Animal> não serve (veja o fatiamento); a lista precisa guardar ponteiros para os animais. E, para ninguém precisar de delete, ponteiros inteligentes (Ponteiros Inteligentes):
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
virtual ~Animal() = default;
std::string nome() const { return nome_; }
virtual void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Au au!\n"; }
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Miau!\n"; }
};
class Peixe : public Animal {
public:
Peixe(std::string nome) : Animal{nome} {}
};
int main() {
std::vector<std::unique_ptr<Animal>> petshop;
petshop.push_back(std::make_unique<Cachorro>("Rex"));
petshop.push_back(std::make_unique<Gato>("Mimi"));
petshop.push_back(std::make_unique<Peixe>("Nemo"));
petshop.push_back(std::make_unique<Cachorro>("Thor"));
for (const auto& animal : petshop) {
animal->falar();
}
}
| Trecho | O que faz |
|---|---|
std::vector<std::unique_ptr<Animal>> | Uma lista de "donos" de animais. Cada item aponta para um objeto que pode ser de qualquer classe filha. |
std::make_unique<Cachorro>("Rex") | Cria um Cachorro e devolve um unique_ptr<Cachorro>, que se converte sozinho em unique_ptr<Animal>. |
class Peixe sem falar() | Não é obrigatório substituir: o peixe usa a versão do Animal. |
const auto& animal | Percorre a lista sem copiar (um unique_ptr nem pode ser copiado). |
animal->falar() | A chamada virtual: a seta, porque animal é (um ponteiro inteligente para) um ponteiro. |
virtual ~Animal() = default; | Quando o vetor morrer, cada unique_ptr apaga o seu bicho pelo destrutor certo. |
$ g++ -std=c++23 -Wall -Wextra petshop.cpp -o petshop $ ./petshop Rex: Au au! Mimi: Miau! Nemo: ... « o Peixe não substituiu: usou o do Animal Thor: Au au!
Repare que o laço do main não tem nenhum if perguntando "é cachorro? é gato?". Para acrescentar um papagaio, basta criar a classe e colocar um no vetor: o laço não muda. Esse é o ganho real do polimorfismo.
6. A armadilha do fatiamento
O polimorfismo só funciona por referência ou ponteiro. Se você copia um Cachorro para uma variável do tipo Animal (por valor), só a parte Animal é copiada; o resto é "fatiado" fora, e o que sobra é um Animal de verdade:
void apresentar(Animal a) { // por valor!
a.falar();
}
int main() {
Cachorro rex{"Rex"};
apresentar(rex);
std::vector<Animal> lista;
lista.push_back(rex);
lista[0].falar();
}
$ g++ -std=c++23 -Wall -Wextra fatia.cpp -o fatia $ ./fatia Rex: ... « o parâmetro a é um Animal, cópia da parte Animal do Rex Rex: ... « o vector<Animal> guarda Animals, não Cachorros
Nenhum aviso: o C++ deixa copiar um filho para a base, porque às vezes é isso mesmo que você quer. O falar() é virtual, mas não ajudou: o objeto é um Animal. Conserto: const Animal& no parâmetro e std::vector<std::unique_ptr<Animal>> na lista.
7. final num método
Na aula de Herança, final proibiu herdar de uma classe. Num método virtual, ele proíbe que as classes de baixo o substituam de novo:
class Cachorro : public Animal {
public:
void falar() const final { std::cout << "Au au!\n"; }
};
class Filhote : public Cachorro {
public:
void falar() const override { std::cout << "Au!\n"; }
};
$ g++ -std=c++23 -Wall -Wextra metfinal.cpp -o metfinal
metfinal.cpp:16:10: error: virtual function ‘virtual void Filhote::falar() const’ overriding final function
16 | void falar() const override { std::cout << "Au!\n"; }
| ^~~~~
metfinal.cpp:11:10: note: overridden function is ‘virtual void Cachorro::falar() const’
11 | void falar() const final { std::cout << "Au au!\n"; }
| ^~~~~
"Função virtual Filhote::falar() sobrescrevendo uma função final". (Clang: declaration of 'falar' overrides a 'final' function.) Use final quando a versão de uma classe precisa ser a última palavra, por exemplo por segurança.
Quando dá errado
O que aconteceu: uma de três causas: (1) faltou virtual no método da base; (2) a assinatura da filha é diferente da da base (um const, um tipo de parâmetro), então ela não substitui nada; (3) o objeto foi copiado por valor para uma variável da base (fatiamento).
Como consertar: (1) virtual na base; (2) override na filha, para o compilador apontar a diferença; (3) use referência, ponteiro ou unique_ptr.
O que significa: o método da filha não corresponde a nenhum virtual da base. Compare letra por letra: nome, tipos dos parâmetros, const no fim. Também acontece se você esqueceu o virtual na base. O Clang diz only virtual member functions can be marked 'override' nesse último caso, e non-virtual member function marked 'override' hides virtual member function quando a assinatura difere.
Como consertar: iguale a assinatura à da base, ou acrescente virtual na base.
O que significa: você deu delete (ou um unique_ptr deu) por um ponteiro da base, e o destrutor da base não é virtual. Só o destrutor da base roda; o da filha é pulado.
Como consertar: virtual ~Base() = default; na classe base.
$ g++ -std=c++23 -Wall -Wextra latir.cpp -o latir
latir.cpp: In function ‘void passear(const Animal&)’:
latir.cpp:12:7: error: ‘const class Animal’ has no member named ‘latir’
12 | a.latir();
| ^~~~~
O que significa: a função passear(const Animal& a) chamou a.latir(), um método que só o Cachorro tem. (Clang: no member named 'latir' in 'Animal'.) Por uma referência ou ponteiro de Animal, você só pode chamar o que o Animal declara. Mesmo que o objeto seja um Cachorro, o compilador só conhece o tipo estático.
Como consertar: se todo animal deve saber fazer aquilo, declare o método (virtual) no Animal. Se só cachorros sabem, trate o cachorro como Cachorro nesse trecho do código.
Missão
Missão: o pet shop está mudo: todos dizem .... (1) Faça o falar() do Animal ser virtual e marque os da filha com override. (2) Crie a classe Passaro, que diz Piu piu!, e acrescente um pássaro chamado Piu ao vetor.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
virtual ~Animal() = default;
std::string nome() const { return nome_; }
void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal{nome} {}
void falar() const { std::cout << nome() << ": Au au!\n"; }
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() const { std::cout << nome() << ": Miau!\n"; }
};
int main() {
std::vector<std::unique_ptr<Animal>> petshop;
petshop.push_back(std::make_unique<Cachorro>("Rex"));
petshop.push_back(std::make_unique<Gato>("Mimi"));
for (const auto& animal : petshop) {
animal->falar();
}
}
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Animal {
public:
Animal(std::string nome) : nome_{nome} {}
virtual ~Animal() = default;
std::string nome() const { return nome_; }
virtual void falar() const { std::cout << nome_ << ": ...\n"; }
private:
std::string nome_;
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Au au!\n"; }
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Miau!\n"; }
};
class Passaro : public Animal {
public:
Passaro(std::string nome) : Animal{nome} {}
void falar() const override { std::cout << nome() << ": Piu piu!\n"; }
};
int main() {
std::vector<std::unique_ptr<Animal>> petshop;
petshop.push_back(std::make_unique<Cachorro>("Rex"));
petshop.push_back(std::make_unique<Gato>("Mimi"));
petshop.push_back(std::make_unique<Passaro>("Piu"));
for (const auto& animal : petshop) {
animal->falar();
}
}
A saída da solução no laboratório:
$ ./petshop2 Rex: Au au! Mimi: Miau! Piu: Piu piu!
Referência
| Código | Onde | O que faz |
|---|---|---|
virtual void f() const; | Base | Permite que as filhas substituam f; a chamada por referência/ponteiro usa o objeto de verdade |
void f() const override; | Filha | Substitui o f da base; erro de compilação se não houver o que substituir |
void f() const final; | Filha | Substitui e proíbe as classes de baixo de substituírem de novo |
virtual ~Base() = default; | Base | Destrutor virtual: delete pela base destrói o objeto inteiro |
void g(const Base& b) | Funções | Recebe qualquer filha sem copiar; chamadas virtuais funcionam |
std::vector<std::unique_ptr<Base>> | Listas | Guarda objetos de classes filhas diferentes |
void g(Base b) / std::vector<Base> | Evite | Fatiamento: sobra só a parte da base |
Exercícios
1. A palavra na base
Complete para o método area() poder ser substituído pelas filhas:
2. A palavra na filha
Complete a declaração na filha, para o compilador conferir que ela substitui o método da base:
3. O destrutor da base
Escreva o destrutor virtual padrão da classe Forma (sem corpo, com = default):
4. Animal tem void falar() const sem virtual; Gato tem o seu. O que Animal& a = mimi; a.falar(); executa?
5. Por que escrever override, se o programa funciona sem ele?
6. Qual lista consegue guardar cachorros e gatos e fazer cada um falar do seu jeito?
Resumo
- Polimorfismo: o mesmo código (escrito para a base) faz coisas diferentes conforme o objeto de verdade.
- Sem
virtual, uma referência ou ponteiro da base chama o método da base (tipo estático). virtualna base faz a chamada seguir o objeto de verdade (tipo dinâmico);overridena filha pede ao compilador para conferir a assinatura.- Assinatura diferente sem
override= método novo, defeito silencioso. Comoverride=marked ‘override’, but does not override. - Classe com método virtual precisa de
virtual ~Base() = default;; sem isso, odeletepela base pula o destrutor da filha. - Para listas de objetos diferentes:
std::vector<std::unique_ptr<Base>>. Copiar para a base por valor fatia o objeto. - Próximo passo: e se o
Animalnão devesse ter umfalar()"padrão"? Veja Classes Abstratas.