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.2c1  Clases-base virtuales


Fig. 1

Fig. 2

 
§1  Sinopsis

Hemos señalado que en herencia múltiple, las clases antecesoras no pueden repetirse:

class B { .... };

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

aunque si pueden repetirse indirectamente:

class B { .... };

class C1 : public B { ... };

class C2 : public B { ... };

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

En este caso, cada objeto de la clase D tiene dos subobjetos de la clase B. Es la situación mostrada en el ejemplo de la página anterior ( 4.11.2c).


Si esta duplicidad puede causar problemas, o sencillamente no se desea, puede añadirse la palabra virtual a la declaración de las clases-base, con lo que B es ahora una clase-base virtual y D solo contiene un subobjeto de dicha clase:

class B { .... };

class C1 : virtual public B { ... };

class C2 : virtual public B { ... };

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

La nueva situación se muestra en la figura 1 y en el DAG de la figura 2.

§2  virtual (palabra-clave)

virtual es una palabra-clave C++ que tiene dos acepciones completamente diferentes dependiendo del contexto de su utilización. Utilizada con nombres de clase sirve para controlar aspectos del mecanismo de herencia; utilizada con nombres de funciones-miembro, controla aspectos del polimorfismo y del tipo de enlazado que se utiliza para tales funciones [1].

§2.1  Sintaxis

La sintaxis de la palabra-clave virtual admite dos variantes que reflejan la dualidad de su utilización:

virtual <nombre-de-clase>

virtual <nombre-de-funcion-miembro>


  La primera forma sintáctica, cuando se antepone al nombre de una clase-base, como en el caso anterior, declara una clase-base virtual, que da lugar a un mecanismo denominado herencia virtual (en contraposición con la herencia ordinaria), cuya descripción abordamos en este epígrafe. Ejemplo:

class D : virtual B { /* ... */ };

es equivalente a:

class D : private virtual B { /* ... */ };

Tenga en cuenta que en estos casos, el calificador virtual solo afecta al identificador que le sigue inmediatamente. Ejemplo:

class E : public virtual B, C, D { /* ... */ };

En este caso las clases B, C y D son bases públicas directas, pero solo B es virtual.


  La segunda forma, cuando se aplica a la función miembro de una clase-base, define dicho método como función virtual, lo que permite que las clases derivadas puedan definir diferentes versiones de la misma función-base (virtual) aún en el caso de que coincidan los argumentos (no se trate por tanto de un caso de sobrecarga), y les proporciona un método especial de enlazado (Enlazado Retrasado 1.4.4).

Las funciones virtuales redefinidas en las clases derivadas, solapan u ocultan a la versión definida en la superclase, dándole a las clases a que pertenecen el carácter de polimórficas ( 4.11.8). Todos los aspectos de este segundo uso, se detalla en el apartado correspondiente a las Funciones virtuales ( 4.11.8a). Las clases-base virtuales son clases perfectamente normales, y nada impide que puedan definirse en ellas funciones virtuales.

§3  Herencia virtual

Hemos señalado, que en una herencia múltiple ordinaria, en la que indirectamente se repite una superclase (como en el ejemplo inicial ), los objetos de la clase derivada contienen múltiples subobjetos de la superclase. Esta duplicidad puede evitarse declarando virtuales las superclases. Como ejemplo de aplicación se incluye un caso análogo al estudiado en el capítulo anterior ( 4.11.2c) en el que se utiliza herencia virtual (en esta ocasión utilizamos una versión ejecutable para comprobar las salidas).

#include <iostream>
using namespace std;

class B {
  public:
  int b;
  int b0;
};

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

class C2 : public virtual 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: Ok.!!
    b0 = 0;             // L9: Ok.!!

    C1::b0 = 1;         // L10: Ok.
    C2::b0 = 1;         // L11: Ok.

  }
};

int main() {        // ================
  D d;
  cout << "miembro b0: " << d.C1::b0 << endl; // M2:
  ++d.C2::b0;
  cout << "miembro b0: " << d.C1::b0 << endl; // M4:
  return 0;
}

Figura 3

Fig. 3

Salida:

miembro b0: 1
miembro b0: 2

Comentario:

La figura 3 muestra la posible disposición de los miembros en los objetos de las clases utilizadas. Compárese con la correspondiente figura del capítulo anterior cuando la herencia no es virtual ( Figura). El comportamiento y las medidas adoptadas para evitar los errores son los ya descritos. Con la diferencia de que aquí han desaparecido los errores de las sentencias L8 y L9. La razón es la misma en ambos casos: como se indica en la figura, los objetos de la clase D solo tienen un subobjeto heredado de la superclase B, por lo que no hay ambigüedad al respecto.

Es significativo que a pesar de ser posible la asignación directa de L9. Aún son posibles las sentencias L10 y L11. En realidad, las tres últimas sentencias inicializan al mismo miembro. Esto puede comprobarse en las sentencias de salida M2 y M4, en las que se incrementa el miembro b0 como subobjeto de C2 y se interroga como subobjeto de C1.

§3.1  Para mayor abundamiento, se añade otro ejemplo que muestra como, en una herencia múltiple ordinaria, en la que indirectamente se repite una superclase, los objetos de la clase derivada contienen múltiples subobjetos de la superclase. Su existencia se pone de manifiesto sobrecontrolando adecuadamente los identificadores ( 4.11.2b):

#include <iostream.h>

class B {                 // clase raíz
  public: int pub;
  B() { pub = 0; }        // constructor por defecto
};
class X : public B {};    // L.7
class Y : public B {};    // L.8
class Z : public X, public Y {};

int main () {             // =====================
  Z z1;                   // instancia de Z
    cout << "Valores iniciales:" << endl;
    cout << " z1.pub (de X) == " << z1.X::pub << endl;
    cout << " z1.pub (de Y) == " << z1.Y::pub << endl;
  z1.X::pub = 1;          // sobrecontrolamos el identificador
  z1.Y::pub = 2;          // L.17
    cout << "Valores finales:" << endl;
    cout << " z1.pub (de X) == " << z1.X::pub << endl;
    cout << " z1.pub (de Y) == " << z1.Y::pub << endl;
}

Salida:

Valores iniciales:
 z1.pub (de X) == 0
 z1.pub (de Y) == 0
Valores finales:
 z1.pub (de X) == 1
 z1.pub (de Y) == 2


Cuando no sea deseable esta repetición de subobjetos, la herencia virtual evita la multiplicidad. Observe el resultado del ejemplo anterior declarando las clases X e Y como clases-base virtuales. El programa sería exactamente igual, excepto el cambio de las líneas 7 y 8 por las siguientes:

class X : virtual public B {};    // L.7 bis
class Y : virtual public B {};    // L.8 bis

Salida:

Valores iniciales:
  z1.pub (de X) == 0
  z1.pub (de Y) == 0
Valores finales:
  z1.pub (de X) == 2
  z1.pub (de Y) == 2

En este caso, los identificadores z1.X::pub y z1.Y::pub señalan al mismo elemento, rezón por la que las dos últimas salidas corresponden a la última asignación realizada (línea 17).

§3.1  Constructores de clases-base virtuales

Los constructores de las superclases virtuales son invocados antes que los de cualquier otra superclase no-virtual.

Si la jerarquía de clases contiene múltiples clases-base virtuales, sus respectivos constructores son invocados en el mismo orden que fueron declaradas. Después se invocan los constructores de las clases-base no virtuales, y por último el constructor de la clase derivada.

No obstante lo anterior, si una clase virtual deriva de una superclase no-virtual, el constructor de esta es invocado primero, a fin de que la clase virtual derivada pueda ser construida adecuadamente. Por ejemplo las sentencias:

class X : public Y, virtual public Z {};
X clasex;

originan el siguiente orden de invocación de constructores:

Z();         // inicialización de la clase-base virtual
Y();         // inicialización de la clase-base no-virtual
X();         // inicialización de la clase derivada

  Inicio.


[1]  Esta suerte de sobrecarga que realiza C++ con algunas palabras-clave, con dos o incluso tres significados totalmente distintos, e incluso (a veces) contradictorios, es uno de los muchos reproches que se le hacen al lenguaje, y una de las causas de su complejidad sintáctica y semántica.