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

Você vai aprender
  • O problema: sem virtual, uma referência da base chama o método da base.
  • virtual na base e override na filha, e o que cada um faz.
  • Como chamar pela referência (Animal&) ou pelo ponteiro (Animal*) da base.
  • Como o override transforma 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 final num 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.

NomeO que éEm apresentar(rex)
Tipo estáticoO tipo escrito no códigoAnimal (o parâmetro é const Animal&)
Tipo dinâmicoO tipo do objeto de verdade, na hora de rodarCachorro

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 Animal deve 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 um virtual da 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.

Como o C++ faz isso por dentro

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 chamarSem virtualCom virtual
rex.falar() (o próprio objeto)CachorroCachorro
Animal& r = rex; r.falar()AnimalCachorro
Animal* p = &rex; p->falar()AnimalCachorro
Animal copia = rex; copia.falar()AnimalAnimal (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 como override, 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 do Gato, 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
A regra

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();
    }
}
TrechoO 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& animalPercorre 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

Todos os objetos fazem a mesma coisa (a da base)

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.

"marked ‘override’, but does not override"

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.

"deleting object of polymorphic class type … which has non-virtual destructor"

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.

"‘const class Animal’ has no member named ‘latir’"
$ 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ódigoOndeO que faz
virtual void f() const;BasePermite que as filhas substituam f; a chamada por referência/ponteiro usa o objeto de verdade
void f() const override;FilhaSubstitui o f da base; erro de compilação se não houver o que substituir
void f() const final;FilhaSubstitui e proíbe as classes de baixo de substituírem de novo
virtual ~Base() = default;BaseDestrutor virtual: delete pela base destrói o objeto inteiro
void g(const Base& b)FunçõesRecebe qualquer filha sem copiar; chamadas virtuais funcionam
std::vector<std::unique_ptr<Base>>ListasGuarda objetos de classes filhas diferentes
void g(Base b) / std::vector<Base>EviteFatiamento: 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:

double area() const { return 0; }

2. A palavra na filha

Complete a declaração na filha, para o compilador conferir que ela substitui o método da base:

double area() const { return lado_ * lado_; }

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).
  • virtual na base faz a chamada seguir o objeto de verdade (tipo dinâmico); override na filha pede ao compilador para conferir a assinatura.
  • Assinatura diferente sem override = método novo, defeito silencioso. Com override = marked ‘override’, but does not override.
  • Classe com método virtual precisa de virtual ~Base() = default;; sem isso, o delete pela 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 Animal não devesse ter um falar() "padrão"? Veja Classes Abstratas.