Conversión de tipos de objeto: dynamic_cast y puntero void *

Los tipos de objeto tienen reglas de conversión específicas que se aplican cuando los tipos de variable de origen y destino no coinciden. Las reglas para los tipos integrados ya se han abordado en el capítulo 2.6 Conversión de tipos. Los detalles específicos de la conversión de tipos de estructuras al copiar se describen en la sección Herencia y disposición de estructuras.

Tanto para las estructuras como para las clases, la condición principal para que pueda admitirse la conversión de tipos es que estén relacionadas a lo largo de la cadena de herencia. Los tipos de diferentes ramas de la jerarquía o no relacionados en absoluto no pueden convertirse entre sí.

Las reglas de conversión son diferentes para los objetos (valores) y los punteros.
 

Objetos

Un objeto de un tipo A puede asignarse a un objeto de otro tipo B si este último tiene un constructor que toma un parámetro de tipo A (con variaciones en valor, referencia o puntero, pero normalmente de la forma B(const A &a)). Un constructor de este tipo también se denomina constructor de conversión.

En ausencia de tal constructor explícito, el compilador intentará utilizar un operador de copia implícito, es decir, B::operator=(const B &b), mientras que las clases A y B deben estar en la misma cadena de herencia para que la copia implícita funcione. Conversión de A a B. Si A se hereda de B (incluso no de forma directa, sino indirecta), entonces las propiedades añadidas a A desaparecerán cuando se copien a B. Si B se hereda de A, entonces sólo se copiará la parte de las propiedades que están en A. Estas conversiones no suelen ser bien recibidas.

Además, es posible que el compilador no siempre proporcione el operador de copia implícito. En concreto, si la clase tiene campos con el modificador const, la copia se considera prohibida (véase más adelante).

En el script ShapesCasting.mq5 utilizamos la jerarquía de la clase Shape para demostrar las conversiones de tipo de objeto. En la clase Shape, el campo type se hace constante de forma deliberada, por lo que un intento de convertir (asignar) un objeto Square a un objeto Rectangle termina con un compilador de errores con explicaciones detalladas:

   attempting to reference deleted function 'void Rectangle::operator=(const Rectangle&)'
      function 'void Rectangle::operator=(const Rectangle&)' was implicitly deleted
      because it invokes deleted function 'void Shape::operator=(const Shape&)'
   function 'void Shape::operator=(const Shape&)' was implicitly deleted
      because member 'type' has 'const' modifier

Según este mensaje, el método de copia Rectangle::operator=(const Rectangle&) fue eliminado implícitamente por el compilador (que proporciona su implementación por defecto) porque utiliza un método similar en la clase base Shape::operator =(const Shape&), que a su vez fue eliminado debido a la presencia del campo type con el modificador const. Tales campos sólo se pueden establecer cuando se crea el objeto, y el compilador no sabe cómo copiar el objeto bajo tal restricción.

Por cierto: el efecto de «borrar» métodos está disponible no sólo para el compilador, sino también para el programador de la aplicación: se hablará más de esto en la sección Control de la herencia: final y supresión.

El problema podría resolverse eliminando el modificador const o proporcionando su propia implementación del operador de asignación (en ella, el campo const no interviene y guardará el contenido con una descripción del tipo «Rectángulo»):

   Rectangle *operator=(const Rectangle &r)
   {
      coordinates.x = r.coordinates.x;
      coordinates.y = r.coordinates.y;
      backgroundColor = r.backgroundColor;
      dx = r.dx;
      dy = r.dy;
      return &this;
   }

Observe que esta definición devuelve un puntero al objeto actual, mientras que la implementación por defecto generada por el compilador era del tipo void (como se ve en el mensaje de error). Esto significa que los operadores de asignación por defecto proporcionados por el compilador no pueden utilizarse en la cadena x = y = z. Si necesita esta capacidad, sobrescriba operator= explícitamente y devuelva el tipo deseado distinto de void.

 

Punteros

Lo más práctico es convertir punteros a objetos de distintos tipos.

En teoría, todas las opciones para convertir punteros de tipo objeto pueden reducirse a tres:

  • De base a derivado, la conversión de tipos hacia abajo (downcast), ya que es habitual dibujar una jerarquía de clases con un árbol invertido;
  • De derivado a base, la conversión de tipos ascendente (upcast);
  • Entre clases de diferentes ramas de la jerarquía o incluso de diferentes familias.

La última opción está prohibida (obtendremos un error de compilación). El compilador permite las dos primeras, pero si «upcast» es natural y segura, «downcast» puede provocar errores en tiempo de ejecución.

void OnStart()
{
   Rectangle *r = addRandomShape(Shape::SHAPES::RECTANGLE);
   Square *s = addRandomShape(Shape::SHAPES::SQUARE);
   Circle *c = NULL;
   Shape *p;
   Rectangle *r2;
   
   // OK
   p = c;   // Circle -> Shape
   p = s;   // Square -> Shape
   p = r;   // Rectangle -> Shape
   r2 = p;  // Shape -> Rectangle
   ...
};

Por supuesto, cuando se utiliza un puntero a un objeto de la clase base, no se puede llamar a métodos y propiedades de la clase derivada, aun cuando el objeto correspondiente se encuentre en el puntero. Obtendremos un error de compilación «identificador no declarado».

Sin embargo, la sintaxis de la conversión explícita está admitida para los punteros (véase estilo C), lo que permite la conversión «sobre la marcha» de un puntero al tipo requerido en expresiones y su desreferenciación sin crear una variable intermedia.

Base *b;
Derived d;
b = &d;
((Derived *)b).derivedMethod();

Aquí hemos creado un objeto de clase derivada (Derived) y un puntero de tipo base al mismo (Base *). Para acceder al método derivedMethod de una clase derivada, el puntero se convierte temporalmente al tipo Derived.

Un tipo de puntero asterisco debe ir entre paréntesis. Además, la propia expresión de conversión, incluido el nombre de la variable, también está rodeada de otro par de paréntesis.

Otro error de compilación («type mismatch»: «falta de correspondencia de tipos») en nuestra prueba genera una línea en la que intentamos convertir un puntero a Rectangle en un puntero a Circle: proceden de ramas de herencia diferentes.

   c = r// error: type mismatch

Las cosas son mucho peores cuando el tipo del puntero que se desea convertir no coincide con el objeto real (aunque sus tipos son compatibles, y por lo tanto el programa compila bien). Una operación de este tipo terminará con un error ya en la fase de ejecución del programa (es decir, el compilador no podrá detectarlo). A continuación, se descarga el programa.

Por ejemplo, en el script ShapesCasting.mq5 hemos descrito un puntero a Square y le hemos asignado un puntero a Shape, que contiene el objeto Rectangle.

   Square *s2;
   // RUNTIME ERROR
   s2 = p// error: Incorrect casting of pointers

El terminal devuelve el error «conversión incorrecta de punteros». El puntero de tipo más específico Square no es capaz de apuntar al objeto progenitor Rectangle.

Para evitar problemas en tiempo de ejecución e impedir que el programa se bloquee, MQL5 proporciona un operador especial: dynamic_cast. Con esta construcción, puede comprobar «cuidadosamente» si es posible convertir un puntero en el tipo requerido. Si la conversión es posible, se llevará a efecto. Si no, obtendremos un puntero nulo (NULL) y podremos procesarlo de forma especial (por ejemplo, utilizando if para inicializar o interrumpir de alguna forma la ejecución de la función, pero no de todo el programa).

La sintaxis de dynamic_cast es la siguiente:

dynamic_castClass * >( pointer)

En nuestro caso, basta con escribir:

   s2 = dynamic_cast<Square *>(p); // trying to cast type, and will get NULL if unsuccessful
   Print(s2); // 0

El programa se ejecutará según lo esperado.

En concreto, podemos volver a intentar convertir un rectángulo en un círculo y asegurarnos de que obtenemos 0:

   c = dynamic_cast<Circle *>(r); // trying to cast type, and will get NULL if unsuccessful
   Print(c); // 0

Hay un tipo de puntero especial en MQL5 que puede almacenar cualquier objeto. Este tipo tiene la siguiente notación: void *.

Vamos a demostrar cómo funciona la variable void * con dynamic_cast.

   void *v;
   v = s;   // set to the instance Square
   PRT(dynamic_cast<Shape *>(v));
   PRT(dynamic_cast<Rectangle *>(v));
   PRT(dynamic_cast<Square *>(v));
   PRT(dynamic_cast<Circle *>(v));
   PRT(dynamic_cast<Triangle *>(v));

Las tres primeras líneas registrarán el valor del puntero (un descriptor del mismo objeto) y las dos últimas imprimirán 0.

Ahora, volvamos al ejemplo de la declaración forward del archivo Indicadores (véase el archivo ThisCallback.mq5), donde las clases Manager y Element contenían punteros mutuos.

El tipo de puntero void * le permite deshacerse de la declaración preliminar (ThisCallbackVoid.mq5). Vamos a comentar la línea que lo contiene, y cambiemos el tipo del campo owner con un puntero al objeto gestor a void *. En el constructor, cambiamos también el tipo del parámetro.

// class Manager; 
class Element
{
   void *owner// looking forward to being compatible with the Manager type *
public:
   Element(void *t = NULL): owner(t) { } // was Element(Manager &t)
   void doMath()
   {
      const int N = 1000000;
      
      // get the desired type at runtime
      Manager *ptr = dynamic_cast<Manager *>(owner);
      // then everywhere you need to check ptr for NULL before using
      
      for(int i = 0i < N; ++i)
      {
         if(i % (N / 20) == 0)
         {
            if(ptr != NULLptr.progressNotify(&thisi * 100.0f / N);
         }
         // ... lots of calculations
      }
      if(ptr != NULLptr.progressNotify(&this100.0f);
   }
   ...
};

Este enfoque puede proporcionar más flexibilidad, pero requiere más cuidado porque dynamic_cast puede devolver NULL. Se recomienda, siempre que sea posible, utilizar facilidades de envío estándar (estáticas y dinámicas) con control de los tipos proporcionados por el lenguaje.

Los punteros void * suelen ser necesarios en casos excepcionales. Y no es el caso de la línea «extra» con una descripción preliminar: se ha utilizado aquí sólo como el ejemplo más sencillo de la universalidad del puntero void *.