Varias clases base

Una clase puede ser derivada de más de una clase base. En un modelo de herencia múltiple (en el que las clases derivan de más de una clase base), las clases base se especifican utilizando el elemento de gramática de la lista base. Por ejemplo, se puede especificar la declaración de clase para CollectionOfBook, derivada de Collection y Book:

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

El orden en el que se especifican las clases base no es significativo, excepto en determinados casos en los que se invocan constructores y destructores. En estos casos, el orden en que se especifican las clases base afecta a lo siguiente:

  • Orden en el que se llama a los constructores. Si el código se basa en la parte Book de CollectionOfBook que se va inicializar antes que la parte Collection, el orden de especificación es significativo. La inicialización tiene lugar en el orden en que se especifican las clases en la lista base.

  • El orden en que los destructores se invocan para la limpieza. De nuevo, si una "parte concreta" de la clase debe estar presente cuando se está destruyendo la otra parte, el orden es significativo. Los destructores se llaman en el orden inverso al de las clases especificadas en la lista base.

    Nota:

    El orden de especificación de clases base puede afectar al diseño de memoria de la clase. No se deben tomar decisiones de programación basadas en el orden de los miembros base en la memoria.

Al especificar la lista base, no se puede especificar el mismo nombre de clase más de una vez. Sin embargo, es posible que una clase sea una base indirecta en una clase derivada más de una vez.

Clases base virtuales

Dado que una clase puede ser una clase base indirecta de una clase derivada más de una vez, C++ proporciona una manera de optimizar el funcionamiento de esas clases base. Las clases base virtuales proporcionan una manera de ahorrar espacio y evitar la ambigüedad en las jerarquías de clases que usan la herencia múltiple.

Cada objeto no virtual contiene una copia de los miembros de datos definidos en la clase base. Esta duplicación desperdicia espacio y requiere especificar qué copia de los miembros de la clase base se desea siempre que se accede a ellos.

Cuando una clase base se especifica como base virtual, puede actuar como base indirecta más de una vez sin la duplicación de sus miembros de datos. Todas las clases base que utilizan una clase base como base virtual comparten una única copia de sus miembros de datos.

Al declarar una clase base virtual, la virtual palabra clave aparece en las listas base de las clases derivadas.

Considere la jerarquía de clases de la ilustración siguiente, que muestra una línea de almuerzo simulada:

Diagram of a simulated lunch line.

La clase base es Queue. La cola del cajero y la cola de almuerzo heredan de Queue. Por último, Lunch Cashier Queue hereda de Cashier Queue y Lunch Queue.

Gráfico simulado de línea de almuerzo

En la ilustración, Queue es la clase base de CashierQueue y LunchQueue. Sin embargo, cuando ambas clases se combinan para formar LunchCashierQueue, surge el siguiente problema: la nueva clase contiene dos subobjetos de tipo Queue, uno de CashierQueue y otro de LunchQueue. En la ilustración siguiente se muestra el diseño de memoria conceptual (el diseño de memoria real podría optimizarse):

Diagram of a simulated lunch line object.

En la ilustración se muestra un objeto Lunch Cashier Queue con dos subobjetos: Cola de cajeros y Cola del almuerzo. Tanto la cola de cajeros como la cola del almuerzo contienen un subobjeto Queue".

Objeto de línea de almuerzo simulado

Hay dos Queue subobjetos en el LunchCashierQueue objeto . El código siguiente declara Queue como clase 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 {};

La virtual palabra clave garantiza que solo se incluya una copia del subobjeto Queue (consulte la siguiente figura).

Diagram of a simulated lunch line object, with virtual base classes depicted.

El diagrama muestra un objeto Lunch Cashier Queue, que contiene un subobjeto Cola de cajeros y un subobjeto Cola de almuerzos. Tanto la cola de cajeros como la cola de almuerzo comparten el mismo subobjeto Queue.

Objeto de línea de almuerzo simulado con clases base virtuales

Una clase puede tener un componente virtual y un componente no virtual de un tipo determinado. Esto sucede en las condiciones que se muestran en la ilustración siguiente:

Diagram of virtual and non virtual components of a class.

El diagrama muestra una clase base de cola. Una clase Cashier Queue y la clase Lunch Queue heredan virtualmente de Queue. Una tercera clase, Takeout Queue, hereda no virtualmente de la cola. Lunch Cashier Queue hereda de Cashier Queue y Lunch Queue. Lunch Takeout Cashier Queue hereda de Lunch Cashier Queue y Takeout Queue.

Componentes virtuales y no virtuales de la misma clase

En la ilustración, CashierQueue y LunchQueue usan Queue como clase base virtual. Sin embargo, TakeoutQueue especifica Queue como clase base, no como una clase base virtual. Por consiguiente, LunchTakeoutCashierQueue tiene dos subobjetos de tipo Queue: uno en la ruta de herencia que incluye LunchCashierQueue y otro en la ruta que incluye TakeoutQueue. Esto se muestra en la ilustración siguiente.

Diagram of the object layout for virtual and non virtual inheritance.

Se muestra un objeto Lunch Takeout Cashier Queue que contiene dos subobjetos: una cola de takeout (que contiene un subobjeto Queue) y una cola de cajeros de almuerzo. El subobjeto Lunch Cashier Queue contiene un subobjeto Cola de cajeros y un subobjeto Cola de almuerzos, ambos que comparten un subobjeto Queue.

Diseño de objeto con herencia virtual y no virtual

Nota:

La herencia virtual supone una importante ventaja con respecto al tamaño si se compara con la herencia no virtual. Sin embargo, puede agregar una sobrecarga de procesamiento.

Si una clase derivada invalida una función virtual que hereda de una clase base virtual y si un constructor o un destructor para la clase base derivada llama a esa función mediante un puntero a la clase base virtual, el compilador puede introducir otros campos ocultos "vtordisp" en las clases con bases virtuales. La /vd0 opción del compilador suprime la adición del miembro de desplazamiento oculto del constructor/destructor vtordisp. La /vd1 opción del compilador, el valor predeterminado, les permite dónde son necesarios. Desactive vtordisps solo si está seguro de que todos los constructores y destructores de clase llaman a funciones virtuales virtualmente.

La /vd opción del compilador afecta a todo un módulo de compilación. Utilice el vtordisp pragma para suprimir y volver a activar vtordisp los campos clase por clase:

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

Ambigüedades en nombres

La herencia múltiple introduce la posibilidad de que los nombres se hereden a lo largo de más de una ruta. Los nombres de miembro de clase a lo largo de estas rutas de acceso no son necesariamente únicos. Estos conflictos de nombre se denominan "ambigüedades".

Cualquier expresión que haga referencia a un miembro de clase debe producir una referencia ambigua. En el ejemplo siguiente se muestra cómo se desarrollan las ambigüedades:

// 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 las declaraciones de clase anteriores, el código como el siguiente es ambiguo porque no está claro si b hace referencia a b en A o en B:

C *pc = new C;

pc->b();

Considere el ejemplo anterior. Dado que el nombre a es un miembro de la clase A y la clase B, el compilador no puede distinguir lo que designa la función a la que a se va a llamar. El acceso a un miembro es ambiguo si puede hacer referencia a más de una función, objeto, tipo o enumerador.

El compilador detecta las ambigüedades realizando pruebas en este orden:

  1. Si el acceso al nombre es ambiguo (como se acaba de describir), se genera un mensaje de error.

  2. Si las funciones sobrecargadas son inequívocas, se resuelven.

  3. Si el acceso al nombre infringe el permiso de acceso a miembros, se genera un mensaje de error. (para obtener más información, consulte Control de acceso de los miembros).

Cuando una expresión produce una ambigüedad en la herencia, la puede resolver manualmente calificando el nombre en cuestión con su nombre de clase. Para que la compilación del ejemplo anterior se realice correctamente sin ambigüedades, utilice código como el siguiente:

C *pc = new C;

pc->B::a();

Nota:

Cuando se declara C, tiene la posibilidad de producir errores cuando se hace referencia a B en el ámbito de C. Sin embargo, no se emite ningún error hasta se realice realmente una referencia no calificada a B en el ámbito de C.

Dominación

Es posible que se llegue a más de un nombre (función, objeto o enumerador) a través de un gráfico de herencia. Estos casos se consideran ambiguos con las clases base no virtuales. También son ambiguos con clases base virtuales, a menos que uno de los nombres "domina" a los demás.

Un nombre domina otro nombre si se define en ambas clases y una clase se deriva de la otra. El nombre dominante es el nombre de la clase derivada; este nombre se utiliza cuando podría producirse una ambigüedad, como se muestra en el ejemplo siguiente:

// 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.
};

Conversiones ambiguas

Las conversiones explícitas e implícitas de punteros o referencias a los tipos de clase pueden producir ambigüedades. En la ilustración siguiente, Conversión ambigua de punteros a clases base, se muestra lo siguiente:

  • La declaración de un objeto de tipo D.

  • Efecto de aplicar el operador de dirección (&) a ese objeto. El operador address-of siempre proporciona la dirección base del objeto.

  • El efecto de convertir explícitamente el puntero obtenido mediante el operador address-of al tipo de clase base A. La coercción de la dirección del objeto al tipo A* no siempre proporciona al compilador suficiente información sobre el subobjeto de tipo A que se va a seleccionar; en este caso, existen dos subobjetos.

Diagram showing how the conversion of pointers to base classes can be ambiguous.

En primer lugar, el diagrama muestra una jerarquía de herencia: A es la clase base. B y C heredan de A. D hereda de B y C. A continuación, se muestra el diseño de memoria para el objeto D. Hay tres subobjetos en D: B (que incluye un subobjeto A) y C (que incluye un subobjeto A). El código y d apunta a la A en el subobjeto B. El código ( * A ) y d apunta al subobjeto B y al subobjeto C.

Conversión ambigua de punteros a clases base

La conversión al tipo A* (puntero a A) es ambigua porque no hay ninguna manera de distinguir qué subobjeto de tipo A es el correcto. Puede evitar la ambigüedad especificando explícitamente qué subobjeto quiere usar, como se indica a continuación:

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

Ambigüedades y clases base virtuales

Si se utilizan clases base virtuales, se puede tener acceso a las funciones, objetos, tipos y enumeradores a través de rutas de herencia múltiple. Dado que solo hay una instancia de la clase base, no hay ambigüedad al acceder a estos nombres.

En la ilustración siguiente se muestra cómo se componen los objetos mediante herencia virtual y no virtual.

Diagram showing virtual derivation and nonvirtual derivation.

En primer lugar, el diagrama muestra una jerarquía de herencia: A es la clase base. B y C heredan virtualmente de A. D virtualmente hereda de B y C. A continuación, se muestra el diseño de D. D contiene subobjetos B y C, que comparten subobjeto A. A continuación, el diseño se muestra como si la misma jerarquía se hubiera derivado mediante la herencia no virtual. En ese caso, D contiene los subobjetos B y C. B y C contienen su propia copia del subobjeto A.

Derivación virtual y no virtual

En la ilustración, el acceso a cualquier miembro de la clase A a través de clases base no virtuales produce ambigüedad; el compilador no tiene información que explique si se debe usar el subobjeto asociado a B o el subobjeto asociado a C. Sin embargo, cuando A se especifica como una clase base virtual, no hay ninguna pregunta a la que se accede al subobjeto.

Consulte también

Herencia