Como se puede adivinar, desde el momento que existe algo conocido
como upcasting - mover en sentido ascendente por una jerarquía de
herencia - debe existir el
downcasting para mover en sentido
descendente en una jerarquía. Pero el upcasting es sencillo porque
al movernos en sentido ascendente en la jerarquía de clases
siempre convergemos en clases más generales. Es decir, cuando se
hace un upcast siempre se está en una clase claramente derivada de
un ascendente (normalmente solo uno, excepto en el caso de
herencia múltiple) pero cuando se hace downcast hay normalmente
varias posibilidades a las que amoldarse. Mas concretamente, un
Circulo
es un tipo de
Figura
(que sería su
upcast), pero si se intenta hacer
un downcast de una
Figura
podría ser un
Circulo
, un
Cuadrado
, un
Triángulo
, etc. El problema es encontrar un
modo seguro de hacer downcast
(aunque es incluso más importante preguntarse por qué se está
usando downcasting en vez de usar
el polimorfismo para que adivine automáticamente el tipo
correcto. En el Volumen 2 de este libro se trata como evitar el
downcasting.
C++ proporciona un moldeado explícito especial (introducido en el capítulo 3) llamado "moldeado dinámico" (dynamic_cast) que es una operación segura. Cuando se usa moldeado dinámico para intentar hacer un molde a un tipo en concreto, el valor de retorno será un puntero al tipo deseado sólo si el molde es adecuado y tiene éxito, de otra forma devuelve cero para indicar que no es del tipo correcto. Aquí tenemos un ejemplo mínimo:
//: C15:DynamicCast.cpp #include <iostream> using namespace std; class Pet { public: virtual ~Pet(){}}; class Dog : public Pet {}; class Cat : public Pet {}; int main() { Pet* b = new Cat; // Upcast // Try to cast it to Dog*: Dog* d1 = dynamic_cast<Dog*>(b); // Try to cast it to Cat*: Cat* d2 = dynamic_cast<Cat*>(b); cout << "d1 = " << (long)d1 << endl; cout << "d2 = " << (long)d2 << endl; } ///:~
Listado 15.19. C15/DynamicCast.cpp
Cuando se use moldeado dinámico, hay que
trabajar con una jerarquía polimórfica real - con funciones
virtuales - debido a que el modeado dinámico
usa información almacenada en la VTABLE para determinar el tipo
actual. Aquí, la clase base contiene un destructor virtual y esto
es suficiente. En el main()
, un puntero a
Cat
es elevado a
Pet
, y después se hace un downcast tanto a
puntero Dog
como a puntero a
Cat
. Ambos punteros son imprimidos, y se
puede observar que cuando se ejecuta el programa el
downcast incorrecto produce el
valor cero. Por supuesto somos los responsables de comprobar que
el resultado del cast no es cero cada vez que se haga un
downcast. Además no hay que asumir que el puntero será exactamente
el mismo, porque a veces se realizan ajustes de punteros durante
el upcasting y el
downcasting (en particular, con la
herencia múltiple).
Un moldeado dinámico requiere un poco de sobrecarga extra en ejecución; no mucha, pero si se está haciendo mucho moldeado dinámico (en cuyo caso debería ser cuestionado seriamente el diseño del programa) se convierte en un lastre en el rendimiento. En algunos casos se puede tener alguna información especial durante el downcasting que permita conocer el tipo que se está manejando, con lo que la sobrecarga extra del modeado dinámico se vuelve innecesario, y se puede usar de manera alternativa un moldeado estático. Aquí se muestra como funciona:
//: C15:StaticHierarchyNavigation.cpp // Navigating class hierarchies with static_cast #include <iostream> #include <typeinfo> using namespace std; class Shape { public: virtual ~Shape() {}; }; class Circle : public Shape {}; class Square : public Shape {}; class Other {}; int main() { Circle c; Shape* s = &c; // Upcast: normal and OK // More explicit but unnecessary: s = static_cast<Shape*>(&c); // (Since upcasting is such a safe and common // operation, the cast becomes cluttering) Circle* cp = 0; Square* sp = 0; // Static Navigation of class hierarchies // requires extra type information: if(typeid(s) == typeid(cp)) // C++ RTTI cp = static_cast<Circle*>(s); if(typeid(s) == typeid(sp)) sp = static_cast<Square*>(s); if(cp != 0) cout << "It's a circle!" << endl; if(sp != 0) cout << "It's a square!" << endl; // Static navigation is ONLY an efficiency hack; // dynamic_cast is always safer. However: // Other* op = static_cast<Other*>(s); // Conveniently gives an error message, while Other* op2 = (Other*)s; // does not } ///:~
Listado 15.20. C15/StaticHierarchyNavigation.cpp
En este programa, se usa una nueva característica que no será
completamente descrita hasta el Volumen 2 de este libro, donde hay
un capítulo que cubre este tema: Información de tipo en
tiempo de ejecución en C++ o mecanismo RTTI
(run time type information). RTTI
permite descubrir información de tipo que ha sido perdida en el
upcasting. El moldeado dinámico es
actualmente una forma de RTTI. Aquí se usa la palabra reservada
typeid
(declarada en el fichero cabecera
typeinfo
) para detectar el tipo de los
punteros. Se puede ver que el tipo del puntero a
Figura
es comparado de forma sucesiva con
un puntero a Circulo
y con un
Cuadrado
para ver si existe alguna
coincidencia. Hay más RTTI que el typeid
, y se
puede imaginar que es fácilmente implementable un sistema de
información de tipos usando una función virtual.
Se crea un objeto Circulo
y la dirección es
elevada a un puntero a Figura
; la segunda
versión de la expresión muestra como se puede usar
modeado estático para ser más explícito con el
upcast. Sin embargo, desde el momento que un upcast siempre es
seguro y es una cosa que se hace comunmente, considero que un cast
explícito para hacer upcast ensucia el código y es innecesario.
Para determinar el tipo se usa RTTI, y se usa modelado estático para realizar el downcast. Pero hay que resaltar que, efectivamente, en este diseño el proceso es el mismo que usar el moldeado dinámico, y el programador cliente debe hacer algún test para descubrir si el cast tuvo éxito. Normalmente se prefiere una situación más determinista que la del ejemplo anterior para usar el modeado estático antes que el moldeado dinámico (y hay que examinar detenidamente el diseño antes de usar moldeado dinámico).
Si una jerarquía de clases no tiene funciones
virtual
es (que es un diseño cuestionable) o si
hay otra información que permite hacer un downcast seguro, es un
poco más rápido hacer el downcast de forma estática que con el
moldeado dinámico. Además, modeado
estático no permitirá realizar un cast fuera de la
jerarquía, como un cast tradicional permitiría, por lo que es más
seguro. Sin enbargo, navegar de forma estática por la jerarquía de
clases es siempre arriesgado por lo que hay que usar
moldeado dinámico a menos que sea una
situación especial.