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 moverse en sentido ascendente en una jerarquía de herencia las clases siempre convergen en clases más generales. Es decir, cuando se hace un upcast siempre se está en una clase claramente derivada de una clase antecesora (normalmente sólo una, excepto en el caso de herencia múltiple) pero cuando se hace downcast hay normalmente varias posibilidades a las que amoldarse. Más concretamente, un Circle es un tipo de Shape (que sería su upcast), pero si se intenta hacer un downcast de un Shape podría ser un Circle, un Square, un Triangle, 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 averigüe automáticamente el tipo correcto. En el Volumen 2 de este libro se trata cómo evitar el downcasting).

C++ proporciona un molde explícito especial (introducido en el Capítulo 3) llamado dynamic_cast que es una operación de downcast segura respecto al tipo. Cuando se usa dynamic_cast para intentar hacer un molde hacia abajo 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. He aquí 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 dynamic_cast, hay que trabajar con una jerarquía polimórfica real - con funciones virtuales - debido a que dynamic_cast usa información almacenada en la VTABLE para determinar el tipo real. Aquí, la clase base contiene un destructor virtual y esto es suficiente. En el main(), se hace upcast de un puntero a Cat a Pet, y después se intenta hacer un downcast tanto a un puntero a Dog como a un puntero a Cat. Se imprimen ambos punteros, y se puede observar que cuando se ejecuta el programa el downcast incorrecto produce el valor cero. Por supuesto, cada vez que se haga un downcast, es responsabilidad del programador comprobar que el resultado del molde no es cero. 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 dynamic_cast requiere un poco de sobrecarga extra en ejecución; no mucha, pero si se está haciendo mucho dynamic_cast (en cuyo caso debería ser cuestionado seriamente el diseño del programa) puede convertirse en un problema de rendimiento. En algunos casos se puede tener alguna información especial durante el downcasting que permita saber con seguridad el tipo que se está manejando, con lo que la sobrecarga extra del dynamic_cast se vuelve innecesaria, y se puede usar en su lugar un static_cast. Así es como podría funcionar:

//: 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. dynamic_cast es en realidad una forma de RTTI. Aquí se usa la palabra reservada typeid (declarada en el fichero de cabecera typeinfo) para detectar el tipo de los punteros. Se puede ver que el tipo del puntero a Shape al que se ha hecho upcast es comparado de forma sucesiva con un puntero a Circle y con un puntero a Square para ver si existe alguna coincidencia. RTTI es algo más que typeid, y también se puede imaginar que sería bastante fácil implementar un sistema propio de información de tipos usando una función virtual.

Se crea un objeto Circle y se hace upcast de su dirección a un puntero a Shape; la segunda versión de la expresión muestra cómo se puede usar static_cast 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 comúnmente, considero que un molde explícito para hacer upcast ensucia el código y es innecesario.

Para determinar el tipo se usa RTTI, y se usa static_cast para realizar el downcast. Pero hay que resaltar que, efectivamente, en este diseño el proceso es el mismo que usar dynamic_cast, y el programador cliente debe hacer alguna comprobación para descubrir qué molde tuvo éxito realmente. Normalmente se querrá una situación más determinista que la del ejemplo anterior antes de usar static_cast en lugar de dynamic_cast (y, de nuevo, hay que examinar detenidamente el diseño antes de usar dynamic_cast).

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 dynamic_cast. Además, static_cast no permitirá realizar un molde fuera de la jerarquía, como sí permitiría el molde tradicional, por lo que es más seguro. Sin embargo, navegar de forma estática por la jerarquía de clases es siempre arriesgado por lo que hay que usar dynamic_cast a menos que se trate de una situación especial.