Object type casting: dynamic_cast and pointer void *

Object types have specific casting rules which apply when source and destination variable types do not match. Rules for built-in types have already been discussed in Chapter 2.6 Type conversion. The specifics of structure type casting of structures when copying were described in the Structure layout and inheritance section.

For both structures and classes, the main condition for the admissibility of type casting is that they should be related along the inheritance chain. Types from different branches of the hierarchy or not related at all cannot be cast to each other.

Casting rules are different for objects (values) and pointers.
 

Objects

An object of one type A can be assigned to an object of another type B if the latter has a constructor that takes a parameter of type A (with variations by value, reference or pointer, but usually of the form B(const A &a)). Such a constructor is also called a conversion constructor.

In the absence of such an explicit constructor, the compiler will try to use an implicit copy operator, i.e. B::operator=(const B &b), while classes A and B must be in the same inheritance chain for the implicit copy to work. conversion from A to B. If A is inherited from B (including not directly, but indirectly), then the properties added to A will disappear when copied to B. If B is inherited from A, then only that part of the properties that are in A will be copied into it. Such conversions are usually not welcome.

Also, the implicit copy operator may not always be provided by the compiler. In particular, if the class has fields with the modifier const, copying is considered prohibited (see further along).

In the script ShapesCasting.mq5, we use the shape class hierarchy to demonstrate object type conversions. In the class Shape, the field type is deliberately made constant, so an attempt to convert (assign) an object Square to an object Rectangle ends with an error compiler with detailed explanations:

   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

According to this message, the copy method Rectangle::operator=(const Rectangle&) was implicitly removed by the compiler (which provides its default implementation) because it uses a similar method in the base class Shape::operator =(const Shape&), which in turn was removed due to the presence of the field type with the modifier const. Such fields can only be set when the object is created, and the compiler does not know how to copy the object under such a restriction.

By the way, the effect of "deleting" methods is available not only to the compiler but to the application programmer: more about this will be discussed in the Inheritance control: final and delete section.

The problem could be solved by removing the modifier const or by providing your own implementation of the assignment operator (in it, the const field is not involved and will save the content with a description of the type: "Rectangle"):

   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;
   }

Note that this definition returns a pointer to the current object, while the default implementation generated by the compiler was of type void (as seen in the error message). This means that the compiler-provided default assignment operators cannot be used in the chain x = y = z. If you require this capability, override operator= explicitly and return the desired type other than void.

 

Pointers

The most practical is to convert pointers to objects of different types.

In theory, all options for casting object type pointers can be reduced to three:

  • From base to derived, the downward type casting (downcast), because it is customary to draw a class hierarchy with an inverted tree;
  • From derivative to base, the ascending type casting (upcast); and
  • Between classes of different branches of the hierarchy or even from different families.

The last option is forbidden (we will get a compilation error). The compiler allows the first two, but if "upcast" is natural and safe, then "downcast" can lead to runtime errors.

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
   ...
};

Of course, when a pointer to an object of the base class is used, methods and properties of the derived class cannot be called on it, even if the corresponding object is located at the pointer. We will get an "undeclared identifier" compilation error.

However, the explicit cast syntax is supported for pointers (see C-style), which allows the "on the fly" conversion of a pointer to the required type in expressions and its dereferencing without creating an intermediate variable.

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

Here we have created a derived class object (Derived) and a base type pointer to it (Base *). To access the method derivedMethod of a derived class, the pointer is temporarily converted to type Derived.

An asterisk pointer type must be enclosed in parentheses. In addition, the cast expression itself, including the variable name, is also surrounded by another pair of parentheses.

Another compilation error ("type mismatch" - "type mismatch") in our test generates a line where we try to cast a pointer to Rectangle to a pointer to Circle: they are from different inheritance branches.

   c = r// error: type mismatch

Things are much worse when the type of the pointer being cast to does not match the actual object (although their types are compatible, and therefore the program compiles fine). Such an operation will end with an error already at the program execution stage (that is, the compiler cannot catch it). The program is then unloaded.

For example, in the script ShapesCasting.mq5 we have described a pointer to Square and assigned it a pointer to Shape, which contains the object Rectangle.

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

The terminal returns the "Incorrect casting of pointers" error. The pointer of a more specific type Square is not capable of pointing to the parent object Rectangle.

To avoid runtime troubles and to prevent the program from crashing, MQL5 provides a special language construct dynamic_cast. With this construct, you can "carefully" check whether it is possible to cast a pointer to the required type. If the conversion is possible, then it will be made. And if not, we will get a null pointer (NULL) and we can process it in a special way (for example, using if to somehow initialize or interrupt the execution of the function, but not the entire program).

The syntax of dynamic_cast is as follows:

dynamic_castClass * >( pointer )

In our case, it is enough to write:

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

The program will run as expected.

In particular, we can try again to cast a rectangle into a circle and make sure that we get 0:

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

There is a special pointer type in MQL5 that can store any object. This type has the following notation: void *.

Let's demonstrate how the variable void * works with 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));

The first three lines will log the value of the pointer (a descriptor of the same object), and the last two will print 0.

Now, back to the example of the forward declaration in the Indicators section (see file ThisCallback.mq5), where the classes Manager and Element contained mutual pointers.

The pointer type void * allows you to get rid of the preliminary declaration (ThisCallbackVoid.mq5). Let's comment out the line with it, and change the type of the field owner with a pointer to the manager object to void *. In the constructor, we also change the type of the parameter.

// 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);
   }
   ...
};

This approach can provide more flexibility but requires more care because dynamic_cast can return NULL. It is recommended, whenever possible, to use standard dispatch facilities (static and dynamic) with control of the types provided by the language.

Pointers void * usually become necessary in exceptional cases. And the "extra" line with a preliminary description is not the case. It has been used here only as the simplest example of the universality of the pointer void *.