Operador dynamic_cast<>

La sintaxis de este operador es:

dynamic_cast<tipo> (<objeto>);

Se usa para hacer cambios de tipo durante la ejecución. Y se usa la base de datos formada por las estructuras type_info que vimos antes.

Este operador sólo puede usarse con objetos polimórficos, cualquier intento de aplicarlo a objetos de tipos fundamentales o agregados o de clases no polimórficas dará como resultado un error.

Hay dos modalidades de dynamic_cast, una usa punteros y la otra referencias:

class Base { // Clase base virtual
...
};
 
class Derivada : public Base { // Clase derivada
...
};

// Modalidad de puntero:
Derivada *p = dynamic_cast<Derivada *> (&Base);
// Modalidad de referencia:
Derivada &refd = dynamic_cast<Derivada &> (Base);

Lo que el programa intentará hacer, durante la ejecución, es obtener bien un puntero o bien una referencia a un objeto de cierta clase base en forma de clase derivada, pero puede que se consiga, o puede que no.

Esto es, en cierto modo, lo contrario a lo que hacíamos al usar polimorfismo. En lugar de obtener un puntero o referencia a la clase base a partir de la derivada, haremos lo contrario: intentar obtener un puntero o referencia a la clase derivada a partir de la clase base.

Volvamos a nuestro ejemplo de Personas.

Vamos a añadir un miembro "sueldo" a la clase "Empleado", y la modificaremos para poder inicializar y leer ese dato.

También crearemos una función "LeerSueldo" que aceptará como parámetro un puntero a un objeto de la clase "Persona":

#include <iostream>
#include <cstring>
using namespace std;
 
class Persona { // Virtual
  public:
   Persona(char *n) { strcpy(nombre, n); }
   virtual void VerNombre() = 0; // Virtual pura
  protected:
   char nombre[30];
};

class Empleado : public Persona {
  public:
   Empleado(char *n, float s) : Persona(n), sueldo(s) {}
   void VerNombre() { 
      cout << "Emp: " << nombre << endl; 
   }
   void VerSueldo() {
      cout << "Salario: " << sueldo << endl; 
   }   
  private:
   float sueldo;
};

class Estudiante : public Persona {
  public:
   Estudiante(char *n) : Persona(n) {}
   void VerNombre() { 
      cout << "Est: " << nombre << endl;
   }
};

void VerSueldo(Persona *p) {
   if(Empleado *pEmp = dynamic_cast<Empleado *> (p))
      pEmp->VerSueldo();
   else
      cout << "No tiene salario." << endl;
}

int main() {
   Persona *Pepito = new Estudiante("Jose");
   Persona *Carlos = new Empleado("Carlos", 1000.0);

   Carlos->VerNombre();
   VerSueldo(Carlos);

   Pepito->VerNombre();
   VerSueldo(Pepito);

   delete Pepito;
   delete Carlos;
   
   return 0;
}

La función "VerSueldo" recibe un puntero a un objeto de la clase base virtual "Persona". Pero a priori no sabemos si el objeto original era un empleado o un estudiante, de modo que no podemos prever si tendrá o no sueldo. Para averiguarlo hacemos un casting dinámico a la clase derivada "Empleado", y si tenemos éxito es que se trata efectivamente de un empleado y mostramos el salario. Si fracasamos, mostramos un mensaje de error.

Pero esto es válido sólo con punteros, si intentamos hacerlo con referencias tendremos un serio problema, ya que no es posible declarar referencias indefinidas. Esto quiere decir que, por una parte, no podemos usar la expresión del casting como una condición en una sentencia if. Por otra parte, si el casting fracasa, se producirá una excepción, ya que se asignará un valor nulo a una referencia durante la ejecución:

void VerSueldo(Persona &p) {
   Empleado rEmp = dynamic_cast<Empleado &> p;
...

Por lo tanto tendremos que usar manejo de excepciones (que es el tema del siguiente capítulo), para usar referencias con el casting dinámico:

void VerSueldo(Persona &p) {
   try {
      Empleado rEmp = dynamic_cast<Empleado &> p;
      rEmp.VerSueldo();
   }
   catch (std::bad_cast) {
      cout << "No tiene salario." << endl;
   }
}

Castings cruzados

Cuando tenemos una clase producto de una derivación múltiple, es posible obtener punteros o referencias a una clase base a partir de objetos o punteros a objetos de otra clase base, eso sí, es necesario usar polimorfismo, no podemos usar un objeto de una clase base para obtener otra. Por ejemplo:

#include <iostream>
using namespace std;

class ClaseA {
  public:
   ClaseA(int x) : valorA(x) {}
   void Mostrar() {
      cout << valorA << endl;
   }
   virtual void nada() {} // Forzar polimorfismo
  private:
   int valorA;
};

class ClaseB {
  public:
   ClaseB(float x) : valorB(x) {}
   void Mostrar() {
      cout << valorB << endl;
   }
   
  private:
   float valorB;
};

class ClaseD : public ClaseA, public ClaseB {
  public:
   ClaseD(int x, float y, char c) : 
      ClaseA(x), ClaseB(y), valorD(c) {}
   void Mostrar() {
      cout << valorD << endl;
   }
   
  private:
   char valorD;
};

int main() {
   ClaseA *cA = new ClaseD(10,15.3,'a');
   ClaseB *cB = dynamic_cast<ClaseB*> (cA);
   
   cA->Mostrar();
   cB->Mostrar();
   
   return 0;
}

Comentarios de los usuarios (2)

Fabio
2015-12-22 20:17:45

Hola tengo una inquietud con el casting cruzado.

Al ver el tamaño de memoria de las clases del ejemplo, obtengo los siguientes resultados:

ClaseA = 8 Bytes, me llamó la atención que la función virtual igual ocupa memoria.

ClaseB = 4 Bytes.

ClaseD = 16 Bytes, que supongo corresponden a los 12 Bytes heredados de las 2 clases anteriores más 4 Bytes por el char.

(Tengo claro que el tamaño puede cambiar según el compilador y el sistema, asi que ese tema lo dejo de lado)

Luego si declaro las variables:

ClaseA varcA(1);
ClaseB varcB(1.1);
ClaseD varcD(2,2.2,'b');

Al ver el tamaño de las variables, estas son igual a de la clase (varcA = 8B, varcB = 4B, varcD = 16B), y de esta forma al hacer un cast de ClaseD a ClaseA, la variable se ajusta al tamaño de la Clase que se ha convertido. luego me fue imposible hacer un dynamic cast, ya que este exige que la variable sea tipo puntero (no se si existe alguna forma de hacer casting cruzado declarando las variables de esa forma).

Y si declaro las variables como puntero:

ClaseA *cA;
ClaseB *cB;
ClaseD *cD;

me llamó la antención que el tamaño de las 3 variables es de 4 Bytes (incluso creando una nueva instancia de las clases), entonces los punteros no almacenan en memoria las funciones virtuales?.

Y ahí va mi inquietud mas grande, tomando en cuenta cA que apunta a ClaseA, entonces los 4Bytes corresponden a un int, luego si en cA creo una nueva instancia de ClaseD:

cA = new ClaseD(1,1.1,'a');

cA sigue teniendo un tamaño de 4Bytes, y a qué apuntan esos 4 Bytes?, al int correspondiente a ClaseA o al char correspondiente a ClaseD?.

y finalmente al hacer el casting cruzado:

cB = dynamic_cast<ClaseB*>(cA);

Cómo el sistema puede obtener el float si para todas las variables apunta a sólo 4 Bytes?, si los valores quedan grabados en los bloques siguientes, no hay peligro que esos valores sean sobreescritos por otro programa?

Espero que entiendan mi duda,

Saludos.

Steven R. Davidson
2015-12-23 02:35:41

Hola Fabio,

Voy comentando a medida que vaya viendo las dudas:

- La función virtual de por sí no ocupa memoria en la clase, pero sí es una consecuencia de ella. La clase es virtual porque contiene una función miembro virtual. Esto significa que internamente guarda un puntero a una "tabla virtual" para realizar polimorfismo.

- Efectivamente, se usa 4 bytes para el 'char', debido al alineamiento de los miembros de la clase.

- El cásting de 'ClaseD' a 'ClaseA' realmente supone un nuevo objeto. Por lo tanto, la variable, 'varcD', no cambia. Esto es igual que el siguiente ejemplo,

int n = 20;
char c = (char) n;

Ni la variable, 'n', ni el valor que contiene son modificados. Puedes pensar que se crea otro valor a partir del valor de 'n' pero de tipo 'char' al que se asigna a la variable, 'c'.

- Como se explica en este capítulo, 'dynamic_cast' sólo funciona con tipos de punteros o con referencias. Por lo tanto, tendrías que hacer algo así,

ClaseA &ref = dynamic_cast< ClaseA & >( varcD );

o si lo prefieres con punteros,

ClaseA *ptr = dynamic_cast< ClaseA * >( &varcD );

- No debería ser una gran sorpresa que los punteros ocupen el mismo tamaño. Al fin y al cabo, se trata de un puntero y por tanto, el valor a guardar es una dirección de memoria, que puede ocupar 4 bytes u 8 bytes, según el compilador y la plataforma que uses.

- Ni las funciones miembro ni las virtuales se guardan en el objeto. Las funciones siempre se guardan por separado. De hecho, los nombres de las clases y sobre todo de las funciones miembro son modificadas o decoradas en C++. Por ejemplo,

void ClaseA :: Mostrar() {...}

se renombra a:

Z6ClaseA7MostrarEv@4

Esto depende del compilador, pero todos "decoran" los nombres.

- Los 4 bytes de 'cA' sirven para guardar una dirección de memoria. Ahora bien, en la primera asignación, 'cA' apunta al comienzo de 'cD' que corresponde a la dirección de memoria del miembro 'ClaseA::valorA'.

- Los punteros no apuntan a 4 bytes, sino que ocupan 4 bytes y apuntan a objetos de diferentes clases. En el caso de la asignación de 'cB', éste apunta al comienzo del objeto de 'ClaseD' que corresponde al objeto de 'ClaseB' que fue heredado. Es decir, 'cB' básicamente guarda la dirección de memoria del miembro 'ClaseB::valorB'.

Es posible que se pueda reescribir, pero típicamente el sistema operativo protege los procesos de tales operaciones de lectura y de escritura; al menos que tenga permiso.

Espero haber aclarado las dudas.

Steven