Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.11.2c   Declaración por herencia múltiple

§1 Sinopsis

La tercera forma de crear una nueva clase es por herencia múltiple, también llamada agregación o composición [1]. Consiste en el ensamblando una nueva clase con los elementos de varias clases-base. C++ permite crear clases derivadas que heredan los miembros de una o más clases antecesoras. Es clásico señalar el ejemplo de un coche, que tiene un motor; cuatro ruedas; cuatro amortiguadores, etc. Elementos estos pertenecientes a la clase de los motores, de las ruedas, los amortiguadores, etc.

Como en el caso de la herencia simple, aparte de los miembros heredados de cada clase antecesora, la nueva clase también puede tener miembros privativos ( 4.11.2b)

§2  Sintaxis

Cuando se declara una clase D derivada de varias clases base: B1, B2, ... se utiliza una lista de las bases directas ( 4.11.2b) separadas por comas. La sintaxis general es:

class-key <info> nomb-clase <: lista-base> { <lista-miembros> };

El significado de cada miembro se indicó al tratar de la declaración de una clase ( 4.11.2). En este caso, la declaración de D seria:

class-key <info> D : <B1, B2, ...> { <lista-miembros> };


D
hereda todos los miembros de las clases antecesoras B1, B2, etc, y solo puede utilizar los miembros que derivan de públicos y protegidos en dichas clases. Resulta así que un objeto de la clase derivada contiene sub-objetos de cada una de las clases antecesoras.

§2.1  Restricciones
Fig-1

Fig. 1

 

  

Fig-2

Fig. 2

     

 

Tenga en cuenta que las clases antecesoras no pueden repetirse, es decir:

class B { ... };

class D : B, B, ... { ... };    // Ilegal!

Aunque la clase antecesora no puede ser base directa más que una vez, si puede repetirse como base indirecta. Es la situación recogida en el siguiente ejemplo cuyo esquema se muestra en la figura 1:

class B { ... };

class C1 : public B { ... };

class C2 : public B { ... };

class D : public C1, C2 { ... };


Aquí la clase D tiene miembros heredados de sus antecesoras D1 y D2, y por consiguiente, dos sub-objetos de la base indirecta B.


El mecanismo sucintamente descrito, constituye lo que se denomina herencia múltiple ordinaria (o simplemente herencia). Como se ha visto, tiene el inconveniente de que si las clases antecesoras contienen elementos comunes, estos se ven duplicados en los objetos de la subclase. Para evitar estos problemas, existe una variante de la misma, la herencia virtual ( 4.11.2c1), en la que cada objeto de la clase derivada no contiene todos los objetos de las clases-base si estos están duplicados.

Las dependencias derivadas de la herencia múltiple suele ser expresada también mediante un grafo denominado DAG ("Direct acyclic graph"), que tiene la ventaja de mostrar claramente las dependencias en casos de composiciones complicadas. La figura 2 muestra el DAG correspondiente al ejemplo anterior. En estos grafos las flechas indican el sentido de la herencia, de forma que A --> B indica que A deriva directamente de B. En nuestro caso se muestra como la clase D contiene dos sub-objetos de la superclase B.

Nota: la herencia múltiple es uno de los puntos peliagudos del lenguaje C++ (y de otros que también implementan este tipo de herencia). Hasta el extremo que algunos teóricos consideran que esta característica debe evitarse, ya que además de las teóricas, presenta también una gran dificultad técnica para su implementación en los compiladores. Por ejemplo, surge la cuestión: si dos clases A y B conforman la composición de una subclase D, y ambas tienen propiedades con el mismo nombre, ¿Que debe resultar en la subclase D? Miembros duplicados, o un miembro que sean la agregación de las propiedades de A y B?. Como veremos a continuación, el creador del C++ optó por un diseño que despeja cualquier posible ambigüedad, aunque ciertamente deriva en una serie de reglas y condiciones bastante intrincadas.

§3  Ambigüedades

La herencia múltiple puede originar situaciones de ambigüedad cuando una subclase contiene versiones duplicadas de sub-objetos de clases antecesoras o cuando clases antecesoras contienen miembros del mismo nombre:

class B {
  public:

  int b;

  int b0;
};

class C1 : public B {
  public:
  int b;
  int c;
};

class C2 : public B {
  public:
  int b;
  int c;
};

class D: public C1, C2 {
  public:
  D() {
    c = 10;             // L1: Error ambigüedad C1::c o C2::c ?
    C1::c = 110;        // L2: Ok.
    C2::c = 120;        // L3: Ok.
    b = 12; Error!!     // L4: Error ambigüedad
    C1::b = 11;         // L5: Ok.   C1::b domina sobre C1::B::b 
    C2::b = 12;         // L6: Ok.   C2::b domina sobre C2::B::b
    C1::B::b = 10;      // L7: Error de sintaxis!
    B::b = 10;          // L8: Error ambigüedad. No existe una única base B
    b0 = 0;             // L9: Error ambigüedad
    C1::b0 = 1;         // L10: Ok.
    C2::b0 = 2;         // L11: Ok.

Figura-3

Fig. 3

  }
};


Los errores originados en el constructor de la clase D son muy ilustrativos sobre los tipos de ambigüedad que puede originar la herencia múltiple (podrían haberse presentado en cualquier otro método D::f() de dicha clase).

En principio, a la vista de la figura 1, podría parecer que las ambigüedades relativas a los miembros de D deberían resolverse mediante los correspondientes especificadores de ámbito:

C1::B::m  // miembros m en C1 heredados de B

C2::B::m  // miembros m en C2 heredados de B

C1::n     // miembros n en C1 privativos

C2::n     // miembros n en C2 privativos


Como puede verse en la sentencia L7 , por desgracia el asunto no es exactamente así (otra de las inconsistencias del lenguaje). El motivo es que el esquema mostrado en la figura es méramente conceptual, y no tiene que corresponder necesariamente con la estructura de los objetos creados por el compilador. En realidad un objeto suele ser una región continua de memoria. Los objetos de las clases derivadas se organizan concatenando los sub-objetos de las bases directas, y los miembros privativos si los hubiere; pero el orden de los elementos de su interior no está garantizado (depende de la implementación). La figura 3 muestra una posible organización de los miembros en el interior de los objetos del ejemplo.

El crador del lenguaje indica al respecto [2] que las relaciones contenidas en un grafo como el de la figura 2 representan información para el programador y para el compilador, pero que esta información no existe en el código final. El punto importante aquí es entender que la organización interna de los objetos obtenidos por herencia múltiple es idéntico al de los obtenidos por herencia simple. El compilador conoce la situación de cada miembro del objeto en base a su posición, y genera el código correspondiente sin indirecciones u otros mecanismos innecesarios (disposición del objeto D en la figura 3).


§3.1  Desde el punto de vista de la sintaxis de acceso, cualquier miembro m privativo de D (zona-5) de un objeto d puede ser referenciado como d.m. Cualquier otro miembro del mismo nombre (m) en alguno de los subobjetos queda eclipsado por este. Se dice que este identificador domina a los demás [3].

Nota: este principio de dominancia funciona también en los subobjetos C1 y C2. Por ejemplo: si un identificador n en el subobjeto C1 está duplicado en la parte privativa de C1 y en la parte heredada de B, C1::n tienen preferencia sobre C1::B::n.


Cualquier objeto c privativo de los subobjetos C1 o C2 (zonas 2 y 4) podría ser accedido como d.c. Pero en este caso existe ambigüedad sobre cual de las zonas se utilizará. Para resolverla se utiliza el especificador de ámbito: C1::c o C2::c. Este es justamente el caso de las sentencias L1/L3 del ejemplo:

c = 10;             // L1: Error ambigüedad C1::c o C2::c ?
C1::c = 110;        // L2: Ok.
C2::c = 120;        // L3: Ok.

Es también el caso de las sentencias L4/L6.  Observe que en este caso no existe ambigüedad respecto a los identificadores b heredados (zonas 1, y 2) porque los de las zonas 2 y 4 tienen preferencia sobre los de las zonas 1 y 2.

b = 12; Error!!     // L4: Error ambigüedad
C1::b = 11;         // L5: Ok.   C1::b domina sobre C1::B::b 
C2::b = 12;         // L6: Ok.   C2::b domina sobre C2::B::b


Es interesante señalar que estos últimos, los identificadores b de las zonas 1 y 2 (heredados de B) no son accesibles porque siempre quedan ocultos por los miembros dominantes, y la gramática C++ no ofrece ninguna forma que permita hacerlo en la disposición actual del ejemplo. Son los intentos fallidos señalados en L7 y L8:

C1::B::b = 10;      // L7: Error de sintaxis!
B::b = 10;          // L8: Error ambigüedad. No existe una unica base B

El error de L8 se refiere a que existen dos posibles candidatos (zonas 1 y 2). Al tratar de la herencia virtual ( 4.11.2c1) veremos un método de resolver (parcialmente) este problema.

Cuando no existe dominancia, los identificadores b0 de las zonas 1 y 2 si son visibles, aunque la designación directa no es posible porque existe ambigüedad sobre la zona 1-2 a emplear. Es el caso de las sentencias L9/L11:

b0 = 0;             // L9: Error ambigüedad
C1::b0 = 1;         // L10: Ok.
C2::b0 = 2;         // L11: Ok.

§4  Modificadores de acceso

Los modificadores de acceso en la lista-base pueden ser cualquiera de los señalados al referirnos a la herencia simple (public, protected y private 4.11.2b-1), y pueden ser distintos para cada uno de los ancestros. Ejemplo:

class D : public B1, private B2, ... { <lista-miembros> };

struct T : private D, E { <lista-miembros> };
                                  // Por defecto E equivale a 'public E'

  Inicio.


[1]  Preferimos el apelativo herencia múltiple, frente al de agregación o composición, porque estos últimos también se utilizan cuando en el cuerpo de una clase se incluyen instancias de otra clase. Por ejemplo:

classA {...};
class B {
   ...
   A a1;
};

[2]  Stroustrup & Ellis:  ACRM  §10.1

[3] Un nombre D::n domina a otro B::n si B es una superclase de D (D deriva de B). En este caso, frente a cualquier ambigüedad que pueda existir entre ambos identificadores se utiliza el nombre dominante (si existe tal posibilidad).