15.12. Downcasting

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 virtuales (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.