Várias classes base

Uma classe pode ser derivada de mais de uma classe base. Em um modelo de várias heranças (no qual as classes são derivadas de mais de uma classe base), as classes base são especificadas usando o elemento de gramática lista base. Por exemplo, a declaração de classe de CollectionOfBook, derivada de Collection e de Book, pode ser especificada:

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

A ordem na qual as classes base são especificadas não é significativa, exceto em determinados casos em que construtores e destruidores são invocados. Nesses casos, a ordem na qual as classes base são especificadas afeta o seguinte:

  • A ordem na qual os construtores são chamados. Se seu código depende da parte Book de CollectionOfBook para ser inicializado antes da parte Collection, a ordem de especificação é significante. A inicialização ocorre na ordem em que as classes são especificadas na lista base.

  • A ordem na qual os destruidores são chamados para limpeza. Novamente, se for necessário que uma "parte" específica da classe esteja presente quando outra parte for destruída, a ordem é importante. Os destruidores são chamados na ordem inversa das classes especificadas na lista base.

    Observação

    A ordem de especificação das classes base pode afetar o layout de memória da classe. Não tome decisões de programação com base na ordem dos membros base na memória.

Ao especificar a lista base, você não pode especificar o mesmo nome de classe mais de uma vez. No entanto, é possível que uma classe seja uma base indireta para uma classe derivada mais de uma vez.

Classes base virtuais

Porque uma classe pode ser uma classe base indireta a uma classe derivada mais de uma vez, C++ fornece uma maneira de otimizar a forma como funcionam as classes base. As classes base virtuais oferecem uma maneira de economizar espaço e evitar ambiguidades nas hierarquias de classes que usam a herança múltipla.

Cada objeto não virtual contém uma cópia dos membros de dados definidos na classe base. Essa duplicação perde espaço e exige que você especifique que cópia dos membros da classe base você quer sempre que os acessa.

Quando uma classe base é especificada como base virtual, ela pode atuar como uma base indireta mais de uma vez sem duplicação de seus membros de dados. Uma única cópia dos membros de dados é compartilhada por todas as classes base que ela usa como base virtual.

Ao declarar uma classe base virtual, a palavra-chave virtual aparece nas listas de base das classes derivadas.

Considere a hierarquia de classes na figura a seguir, que ilustra uma linha de almoço simulada:

Diagrama de uma linha de almoço simulada.

A classe base é Queue. A fila do caixa e a fila do almoço herdam da fila. Finalmente, a fila do caixa do almoço herda da fila do caixa e da fila do almoço.

Gráfico simulado da linha do almoço

Na figura, Queue é a classe base para CashierQueue e LunchQueue. No entanto, quando as duas classes são combinadas para formar LunchCashierQueue, o seguinte problema ocorre: a nova classe contém dois subobjetos do tipo Queue, um de CashierQueue e o outro de LunchQueue. A figura a seguir mostra o layout de memória conceitual (o layout de memória real pode ser otimizado):

Diagrama de um objeto de linha de almoço simulado.

A figura mostra um objeto Fila de Caixa de Almoço com dois subobjetos: Fila de Caixa e Fila de Almoço. Tanto a fila do caixa quanto a fila do almoço contêm um subobjeto Fila."

Objeto de linha de almoço simulado

Há dois Queue subobjetos no LunchCashierQueue objeto. O código a seguir declara Queue como uma classe base virtual:

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

A palavra-chave virtual garante que apenas uma cópia do subobjeto Queue seja incluída (veja a figura a seguir).

Diagrama de um objeto de linha de almoço simulado, com classes base virtuais representadas.

O diagrama mostra um objeto Lunch Cashier Queue, que contém um subobjeto Fila de caixa e um subobjeto Lunch Queue. Tanto a Fila de caixa quanto a Fila de almoço compartilham o mesmo subobjeto Fila.

Objeto de linha de almoço simulado com classes base virtuais

Uma classe pode ter um componente virtual e um componente não virtual de determinado tipo. Isso acontece nas condições ilustradas na figura a seguir:

Diagrama de componentes virtuais e não virtuais de uma classe.

O diagrama mostra uma classe base de fila. Uma classe Fila de Caixa e uma classe Fila de Almoço herdam virtualmente de Fila. Uma terceira classe, Fila para viagem, herda não virtualmente da fila. A fila do caixa do almoço herda da fila do caixa e da fila do almoço. A fila do caixa do almoço para viagem herda da fila do caixa do almoço e da fila do takeaway.

Componentes virtuais e não virtuais da mesma classe

Na figura, CashierQueue e LunchQueue usam Queue como uma classe base virtual. No entanto, TakeoutQueue especifica Queue como uma classe base, não uma classe base virtual. Portanto, LunchTakeoutCashierQueue tem dois subobjetos do tipo Queue: um do caminho de herança que inclui LunchCashierQueue e outro do caminho que inclui TakeoutQueue. Isso é ilustrado na figura a seguir.

Diagrama do layout do objeto para herança virtual e não virtual.

É mostrado um objeto Lunch Takeout Cashier Queue que contém dois subobjetos: um Lunch Queue (que contém um subobjeto Queue) e um Lunch Cashier Queue. O subobjeto Fila de caixa de almoço contém um subobjeto Fila de caixa e um subobjeto Fila de almoço, ambos compartilham um subobjeto Fila.

Layout de objeto com herança virtual e não virtual

Observação

A herança virtual oferece benefícios significativos de tamanho quando comparada com a herança não virtual. No entanto, pode apresentar a sobrecarga adicional de processamento.

Se uma classe derivada substituir uma função virtual herdada de uma classe base virtual e se um construtor ou um destruidor para a classe base derivada chamar essa função usando um ponteiro para a classe base virtual, o compilador poderá introduzir outros campos "vtordisp" ocultos nas classes com bases virtuais. A opção do compilador /vd0 suprime a adição do membro oculto de deslocamento do construtor/destruidor vtordisp. A /vd1 opção do compilador, o padrão, os habilita onde são necessários. Desative vtordisps somente se tiver certeza de que todos os construtores e destruidores de classe chamam funções virtuais virtualmente.

A opção do compilador /vd afeta um módulo de compilação inteiro. Use o pragma vtordisp para suprimir e habilitar novamente os campos vtordisp classe por classe:

#pragma vtordisp( off )
class GetReal : virtual public { ... };
#pragma vtordisp( on )

Ambiguidades de nome

A herança múltipla apresenta a possibilidade de que os nomes sejam herdados ao longo de mais de um caminho. Os nomes de membros de classe ao longo desses caminhos não são necessariamente exclusivos. Esses conflitos de nome são chamados de “ambiguidades”.

Qualquer expressão que se referir a um membro de classe deve fazer uma referência não ambígua. O exemplo a seguir mostra como as ambiguidades se desenvolvem:

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

Dadas as declarações de classe anteriores, o código como o seguinte é ambíguo porque não está claro se b se refere ao b in A ou in B:

C *pc = new C;

pc->b();

Considere o exemplo anterior. Como o nome a é um membro de classe A e classe B, o compilador não pode discernir qual a designa a função a ser chamada. O acesso a um membro é ambíguo se pode referenciar mais de uma função, objeto, tipo ou enumerador.

O compilador detecta ambiguidades executando testes nesta ordem:

  1. Se o acesso ao nome é ambíguo (como somente descrito), será gerada uma mensagem de erro.

  2. Se as funções sobrecarregadas não forem ambíguas, elas serão resolvidas.

  3. Se o acesso ao nome viola a permissão de acesso a membros, será gerada uma mensagem de erro. Para obter mais informações, consulte Member-Access Control.

Quando uma expressão gera uma ambiguidade por meio de uma herança, você pode solucioná-la manualmente qualificando o nome em questão com seu nome de classe. Para fazer com que o exemplo acima seja compilado corretamente sem ambiguidades, use códigos como:

C *pc = new C;

pc->B::a();

Observação

Quando C é declarado, ele tem o potencial para causar erros quando B é referenciado no escopo de C. Nenhum erro é emitido, no entanto, até que uma referência não qualificada a B seja feita no escopo de C.

Dominância

É possível que mais de um nome (função, objeto ou enumerador) seja alcançado por meio de um grafo de herança. Esses casos são considerados ambíguos com classes base não virtuais. Eles também são ambíguos com classes base virtuais, a menos que um dos nomes "domine" os outros.

Um nome domina outro nome se for definido em ambas as classes e uma classe for derivada da outra. O nome dominante é o nome na classe derivada; esse nome é usado quando uma ambiguidade poderia surgir de outra forma, conforme mostrado no seguinte exemplo:

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
    int a;
};

class B : public virtual A {
public:
    int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
    D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

Conversões ambíguas

As conversões explícitas e implícitas de ponteiros ou as referências aos tipos da classe podem causar ambiguidades. A figura a seguir, a conversão ambígua dos ponteiros para as classes base, mostra o seguinte:

  • A declaração de um objeto de tipo D.

  • O efeito de aplicar o operador de endereço (&) a esse objeto. O operador address-of sempre fornece o endereço base do objeto.

  • O efeito de converter explicitamente o ponteiro obtido usando o operador address-of para o tipo de classe base A. Forçar o endereço do objeto para o tipo A* nem sempre fornece ao compilador informações suficientes sobre qual subobjeto do tipo A selecionar; nesse caso, existem dois subobjetos.

Diagrama mostrando como a conversão de ponteiros em classes base pode ser ambígua.

O diagrama primeiro mostra uma hierarquia de herança: A é a classe base. B e C herdam de A. D herda de B e C. Em seguida, o layout da memória é mostrado para o objeto D. Existem três subobjetos em D: B (que inclui um subobjeto A) e C (que inclui um subobjeto A). O código & d aponta para o A no subobjeto B. O código ( * A ) & d aponta para o subobjeto B e o subobjeto C.

Conversão ambígua de ponteiros em classes base

A conversão para tipo A* (ponteiro para A) é ambígua porque não há como discernir qual subobjeto do tipo A é o correto. Você pode evitar a ambiguidade especificando explicitamente qual subobjeto você pretende usar, da seguinte maneira:

(A *)(B *)&d       // Use B subobject.
(A *)(C *)&d       // Use C subobject.

Ambiguidades e classes base virtuais

Se forem usadas classes base virtuais, as funções, os objetos, os tipos e os enumeradores poderão ser alcançados por meio de caminhos de herança múltipla. Como há apenas uma instância da classe base, não há ambiguidade ao acessar esses nomes.

A figura a seguir mostra como os objetos são compostos usando a herança virtual e não virtual.

Diagrama mostrando a derivação virtual e a derivação não virtual.

O diagrama primeiro mostra uma hierarquia de herança: A é a classe base. B e C praticamente herdam de A. D virtualmente herda de B e C. Em seguida, o layout de D é mostrado. D contém os subobjetos B e C, que compartilham o subobjeto A. Em seguida, o layout é mostrado como se a mesma hierarquia tivesse sido derivada usando herança não virtual. Nesse caso, D contém os subobjetos B e C. B e C contêm sua própria cópia do subobjeto A.

Derivação virtual e não virtual

Na figura, o acesso a qualquer membro da classe A por meio de classes base não virtuais causa uma ambiguidade; o compilador não tem nenhuma informação que explique se ele deve usar o subobjeto associado a B ou o subobjeto associado a C. No entanto, quando A é especificado como uma classe base virtual, não há dúvida de qual subobjeto está sendo acessado.

Confira também

Herança