herança virtual

Diagrama da herança diamante , um problema que a herança virtual está tentando resolver

A herança virtual é uma técnica C++ que garante que apenas uma cópia das variáveis ​​de membro de uma classe base seja herdada por classes derivadas netas. Sem herança virtual, se duas classes e herdam de uma classe , e uma classe herda de ambos e , conterá duas cópias das variáveis ​​de membro de ' s: uma via e uma via . Estes serão acessíveis de forma independente, usando resolução de escopo . BCADBCDABC

Em vez disso, se as classes Be Cherdam virtualmente de class A, os objetos de class Dconterão apenas um conjunto de variáveis ​​de membro de class A.

Esse recurso é mais útil para herança múltipla , pois torna a base virtual um subobjeto comum para a classe derivada e todas as classes derivadas dela. Isso pode ser usado para evitar o problema do diamante , esclarecendo a ambigüidade sobre qual classe ancestral usar, pois da perspectiva da classe derivada ( Dno exemplo acima), a base virtual ( A) age como se fosse a classe base direta de D, não uma classe derivada indiretamente por meio de uma base ( Bou C). [1] [2]

É usado quando a herança representa restrição de um conjunto em vez de composição de partes. Em C++, uma classe base destinada a ser comum em toda a hierarquia é denotada como virtual com a virtual palavra-chave .

Considere a seguinte hierarquia de classes.

Herança virtual UML.svg

struct Animal { virtual ~ Animal () = padrão ; vazio virtual Comer () {} };  
       
       


struct Mammal : Animal { virtual void Breathe () {} };   
       


struct WingedAnimal : Animal { virtual void Flap () {} };   
       


// Um ​​morcego é um mamífero alado 
struct Bat : Mammal , WingedAnimal {};    

morcego morcego ; 

Conforme declarado acima, uma chamada para bat.Eaté ambígua porque há duas Animalclasses base (indiretas) em Bat, portanto, qualquer objeto tem dois subobjetos de classe base Batdiferentes . AnimalPortanto, uma tentativa de vincular diretamente uma referência ao Animalsubobjeto de um Batobjeto falharia, pois a vinculação é inerentemente ambígua:

morcego b ; Animais & a = b ; // erro: em qual subobjeto Animal um Morcego deve lançar, // um Mammal::Animal ou um WingedAnimal::Animal? 
     
                

Para eliminar a ambiguidade, seria necessário converter explicitamente batpara qualquer subobjeto de classe base:

morcego b ; Animal & mamífero = static_cast < Mamífero &> ( b ); Animal & alado = static_cast < WingedAnimal &> ( b ); 
    
   

Para chamar Eat, é necessária a mesma desambiguação ou qualificação explícita: static_cast<Mammal&>(bat).Eat()ou static_cast<WingedAnimal&>(bat).Eat()ou alternativamente bat.Mammal::Eat()e bat.WingedAnimal::Eat(). A qualificação explícita não apenas usa uma sintaxe mais fácil e uniforme para ponteiros e objetos, mas também permite despacho estático, portanto, sem dúvida, seria o método preferível.

Nesse caso, a herança dupla de Animalprovavelmente não é desejada, pois queremos modelar que a relação ( Baté um Animal) existe apenas uma vez; que a Baté a Mammale é a WingedAnimalnão implica que seja Animalduas vezes: uma Animalclasse base corresponde a um contrato que Batimplementa (o relacionamento "é um" acima realmente significa " implementa os requisitos de ") e a Batimplementa o Animalcontrato apenas uma vez . O significado do mundo real de " é apenas uma vez" é que Batdeve haver apenas uma maneira de implementar Eat, não duas maneiras diferentes, dependendo se a Mammalvisão do Batestá comendo ou a WingedAnimalvisão doBat. (No primeiro exemplo de código, vemos que Eatnão é substituído em Mammalou WingedAnimal, portanto, os dois Animalsubobjetos realmente se comportarão da mesma forma, mas este é apenas um caso degenerado e isso não faz diferença do ponto de vista do C++.)

Às vezes, essa situação é chamada de herança de diamante (consulte o problema do diamante ) porque o diagrama de herança tem a forma de um diamante. A herança virtual pode ajudar a resolver esse problema.

A solução

Podemos re-declarar nossas classes da seguinte forma:

struct Animal { virtual ~ Animal () = padrão ; vazio virtual Comer () {} };  
       
       


// Duas classes virtualmente herdando Animal: 
struct Mammal : virtual Animal { virtual void Breathe () {} };    
       


struct WingedAnimal : Virtual Animal { virtual void Flap () {} };    
       


// Um ​​morcego ainda é um mamífero alado 
struct Bat : Mammal , WingedAnimal {};    

A Animalparte de Bat::WingedAnimalagora é a mesma Animal instância usada por Bat::Mammal, o que significa que a Battem apenas uma Animalinstância compartilhada em sua representação e, portanto, uma chamada para Bat::Eaté inequívoca. Além disso, uma conversão direta de Batpara Animaltambém é inequívoca, agora que existe apenas uma Animalinstância que Batpode ser convertida para.

A capacidade de compartilhar uma única instância do Animalpai entre Mammale WingedAnimalé habilitada registrando o deslocamento de memória entre os membros Mammalou WingedAnimale os da base Animaldentro da classe derivada. No entanto, esse deslocamento pode, no caso geral, ser conhecido apenas em tempo de execução, portanto, Batdeve se tornar ( vpointer, Mammal, vpointer, WingedAnimal, Bat, Animal). Existem dois ponteiros vtable , um por hierarquia de herança que herda virtualmente Animal. Neste exemplo, um para Mammale outro para WingedAnimal. O tamanho do objeto, portanto, aumentou em dois ponteiros, mas agora há apenas um Animale nenhuma ambiguidade. Todos os objetos do tipo Batusarão os mesmos vpointers, mas cadaBatO objeto conterá seu próprio Animalobjeto exclusivo. Se outra classe herda de Mammal, como Squirrel, então o vpointer na Mammalparte de Squirrelgeralmente será diferente do vpointer na Mammalparte de Batembora eles possam ser os mesmos se a Squirrelclasse tiver o mesmo tamanho que Bat.

Exemplo Adicional de Vários Ancestrais

Este exemplo ilustra um caso em que a classe base Atem uma variável construtora msge um ancestral adicional Eé derivado da classe neto D.

  A  
 / \  
BC  
 \ /  
  D
  |
  E

Aqui, Adeve ser construído em ambos De E. Além disso, a inspeção da variável msgilustra como a classe Ase torna uma classe base direta de sua classe derivada, em oposição a uma classe base de qualquer classe derivada intermediária classificada entre Ae a classe derivada final. O código abaixo pode ser explorado interativamente aqui.

#include <string> #include <iostream> 
 

class A { private : std :: string _msg ; public : A ( std :: string x ) : _msg ( x ) {} void test (){ std :: cout << "olá de A: " << _msg << " \n " ; } };                       
     
          
    
            
            
 

// B,C herdam A virtualmente 
classe B : virtual public A { public : B ( std :: string x ) : A ( "b" ){} }; classe C : virtual public A { public : C ( std :: string x ) : A ( "c" ){} }; // Erro de compilação quando :A("c") é removido (já que o construtor de A não é chamado)            
            

//classe C: virtual public A { public: C(std::string x){} }; 
//classe C: virtual public A { public: C(std::string x){ A("c"); } }; // Mesmo erro de compilação

// Já que B, C herdam A virtualmente, A deve ser construído em cada 
classe filha D : public B , C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} }; classe E : public D { public : E ( std :: string x ) : A ( "e_a"                  
                 ), D ( "e_d" ){} };   

// Erro de compilação sem construir A 
//classe D: public B,C { public: D(std::string x):B(x),C(x){} };

// Erro de compilação sem construir A 
//classe E: public D { public: E(std::string x):D(x){} };


int main ( int argc , char ** argv ){ D d ( "d" ); d . teste (); // olá de A: d_a     
      
     

    E e ( "e" ); e . teste (); // olá de A: e_a }  
     

Métodos Virtuais Puros

Suponha que um método virtual puro seja definido na classe base. Se uma classe derivada herda virtualmente a classe base, o método virtual puro não precisa ser definido nessa classe derivada. No entanto, se a classe derivada não herdar virtualmente a classe base, todos os métodos virtuais deverão ser definidos. O código abaixo pode ser explorado interativamente aqui.

#include <string> #include <iostream> 
 

classe A { protegida : std :: string _msg ; public : A ( std :: string x ) : _msg ( x ) {} void test (){ std :: cout << "olá de A: " << _msg << " \n " ; } virtual void pure_virtual_test () = 0 ; };                       
     
          
    
            
             
            
 

// como B,C herdam A virtualmente, o método virtual puro pure_virtual_test não precisa ser definido 
classe B : virtual public A { public : B ( std :: string x ) : A ( "b" ){} }; classe C : virtual public A { public : C ( std :: string x ) : A ( "c" ){} };             
             

// como B,C herda A virtualmente, A deve ser construído em cada filho 
// entretanto, como D não herda B,C virtualmente, o método virtual puro em A *deve ser definido* 
class D : public B , C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} void pure_virtual_test () override { std :: cout << "pure virtual hello from : "     
     
         
            << _msg << " \n " ; } };  
 

// não é necessário redefinir o método virtual puro depois que o pai o define 
class E : public D { public : E ( std :: string x ) : A ( "e_a" ), D ( "e_d" ){} } ;     
     
       



int main ( int argc , char ** argv ){ D d ( "d" ); d . teste (); // olá de A: d_a d . teste_puro_virtual (); // puro olá virtual de: d_a     
     
     
     

    E e ( "e" ); e . teste (); // olá de A: e_a e . teste_puro_virtual (); // alô virtual puro de: e_a }  
     
     

Referências

  1. ^ Milea, Andrei. "Resolvendo o problema do diamante com herança virtual". Cprogramming. com . Recuperado 2010-03-08 . Um dos problemas que surgem devido à herança múltipla é o problema do diamante. Uma ilustração clássica disso é dada por Bjarne Stroustrup (o criador do C++) no seguinte exemplo:
  2. ^ McArdell, Ralph (2004-02-14). "C++/O que é herança virtual?". Todos os especialistas . Arquivado do original em 2010-01-10 . Recuperado 2010-03-08 . Isso é algo que você acha que pode ser necessário se estiver usando herança múltipla. Nesse caso, é possível que uma classe seja derivada de outras classes que tenham a mesma classe base. Nesses casos, sem herança virtual, seus objetos conterão mais de um subobjeto do tipo base compartilhado pelas classes base. Se este é o efeito necessário depende das circunstâncias. Caso contrário, você pode usar a herança virtual especificando classes base virtuais para os tipos base para os quais um objeto inteiro deve conter apenas um desses subobjetos de classe base.