Herança virtual - Virtual inheritance

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

Herança virtual é um C ++ técnica que assegura que apenas uma cópia de uma classe base ' s membro variáveis são herdadas por classes neto derivado. Sem herança virtual, se duas classes Be Cherdam de uma classe Ae uma classe Dherda de ambos Be C, em seguida, Dirá conter duas cópias de A' variáveis de membro s: uma via B, e uma via C. Eles estarão acessíveis de forma independente, usando resolução de escopo .

Em vez disso, se as classes Be Cherdam virtualmente da classe A, os objetos da classe Dconterão apenas um conjunto de variáveis ​​de membro da classe 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).

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

Considere a seguinte hierarquia de classes.

struct Animal {
    virtual ~Animal() = default;
    virtual void Eat() {}
};

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

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

// A bat is a winged mammal
struct Bat: Mammal, WingedAnimal {};

Bat bat;

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

Bat b;
Animal& a = b;  // error: which Animal subobject should a Bat cast into, 
                // a Mammal::Animal or a WingedAnimal::Animal?

Para eliminar a ambigüidade, seria necessário converter explicitamente batpara qualquer um dos subobjetos da classe base:

Bat b;
Animal& mammal = static_cast<Mammal&>(b); 
Animal& winged = static_cast<WingedAnimal&>(b);

Para chamar Eat, a mesma desambiguação ou qualificação explícita é necessária: 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 uniforme e mais fácil para ponteiros e objetos, mas também permite o envio estático, portanto, seria o método preferível.

Nesse caso, a herança dupla de Animalé provavelmente indesejada, pois queremos modelar que a relação ( Baté uma 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 Batapenas implementa o Animalcontrato uma vez . O significado do mundo real de " é apenas uma vez" é que Batdeve haver apenas uma forma de implementação Eat, não duas formas diferentes, dependendo se a Mammalvisão do Batestá comendo ou a WingedAnimalvisão do Bat. (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 ++.)

Essa situação às vezes é 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 declarar novamente nossas classes da seguinte maneira:

struct Animal {
    virtual ~Animal() = default;
    virtual void Eat() {}
};

// Two classes virtually inheriting Animal:
struct Mammal: virtual Animal {
    virtual void Breathe() {}
};

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

// A bat is still a winged mammal
struct Bat: Mammal, WingedAnimal {};

A Animalparte de Bat::WingedAnimalé agora a mesma Animal instância que a 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::Eatnão é ambígua. Além disso, uma conversão direta de Batpara Animaltambém não é ambígua, agora que existe apenas uma Animalinstância para a qual Batpoderia ser convertida.

A capacidade de compartilhar uma única instância do Animalpai entre Mammale WingedAnimalé ativada pelo registro do deslocamento de memória entre os membros Mammalou WingedAnimale aqueles da base Animaldentro da classe derivada. No entanto, este pode compensar no caso geral só é conhecido em tempo de execução, portanto, Batdeve tornar-se ( vpointer, Mammal, vpointer, WingedAnimal, Bat, Animal). Existem dois ponteiros vtable , um por hierarquia de herança que virtualmente herda Animal. Neste exemplo, um para Mammale um para WingedAnimal. O tamanho do objeto, portanto, aumentou em dois ponteiros, mas agora há apenas um Animale nenhuma ambigüidade. Todos os objetos do tipo Batusarão os mesmos vpointers, mas cada Batobjeto 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 possam acontecer ser os mesmos se a Squirrelclasse tiver o mesmo tamanho que Bat.

Exemplo Adicional

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

  A  
 / \  
B   C  
 \ /  
  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 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<<"hello from A: "<<_msg <<"\n"; } 
}; 

// B,C inherit A virtually
class B: virtual public A   { public: B(std::string x):A("b"){}  }; 
class C: virtual public A   { public: C(std::string x):A("c"){}  }; 

// since B,C inherit A virtually, A must be constructed in each child
class D: public         B,C { public: D(std::string x):A("d_a"),B("d_b"),C("d_c"){}  }; 
class E: public         D   { public: E(std::string x):A("e_a"),D("e_d"){}  }; 

// breaks without constructing A
// class D: public         B,C { public: D(std::string x):B(x),C(x){}  }; 

// breaks without constructing A
//class E: public         D   { public: E(std::string x):D(x){}  }; 


int main(int argc, char ** argv){
    D d("d"); 
    d.test(); // hello from A: d_a

    E e("e"); 
    e.test(); // hello from A: e_a
}

Referências