Приведение объектных типов: dynamic_cast и указатель void *

Объектные типы имеют собственные правила приведения, применяемые, когда типы переменных источника и приемника не совпадают. Правила для встроенных типов уже рассматривались в главе 2.6 Приведение типов. Особенности приведения типов структур при их копировании излагались в разделе Компоновка и наследование структур.

Как для структур, так и для классов главным условием допустимости приведения типов является то, чтобы они были связаны по цепочке наследования. Типы из разных веток иерархии или вообще не связанные родственными отношениями приводить друг к другу нельзя.

Правила приведения различаются для объектов (значений) и указателей.
 

Объекты

Объект одного типа A можно присвоить объекту другого типа B при наличии в последнем конструктора, принимающего параметр типа A (с вариациями по значению, ссылке или указателю, но как правило, вида B(const A &a)). Такой конструктор еще называется конструктором преобразования.

В отсутствие такого явного конструктора, компилятор попытается применить неявный оператор копирования, то есть вида B::operator=(const B &b), при этом классы A и B должны быть в одной цепочке наследования, чтобы сработала неявная конвертация из A в B. Если A унаследован от B (в том числе, не напрямую, а опосредованно), то добавленные в A свойства пропадут при копировании в B. Если же B унаследован от A, то в него скопируется только та часть свойств, что имеется в A. В принципе, такие конвертации не приветствуются.

Кроме того, неявный оператор копирования не всегда может быть предоставлен компилятором. В частности, если в классе имеются поля с модификатором const, копирование считается запрещенным (см. далее).

В скрипте ShapesCasting.mq5 мы используем иерархию классов фигур для демонстрации преобразований объектных типов. В классе Shape поле type намеренно сделано константным, поэтому попытка преобразовать (присвоить) объект Square в объект Rectangle заканчивается ошибкой компилятора с подробными пояснениями:

   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 'typehas 'constmodifier

Согласно этому сообщению, метод копирования Rectangle::operator=(const Rectangle&) был неявным образом удален компилятором (который и предоставляет его реализацию по умолчанию), поскольку использует аналогичный метод базового класса Shape::operator=(const Shape&), а тот, в свою очередь, был удален из-за наличия поля type с модификатором const. Такие поля можно установить только при создании объекта, и компилятор не знает, как копировать объект при таком ограничении.

Между прочим, эффект "удаления" методов доступен не только компилятору, но прикладному программисту: подробнее об этом будет рассказано в разделе Управление наследованием: final и delete.

Решить проблему можно было бы удалением модификатора const или предоставив собственную реализацию оператора присваивания (в ней const-поле не задействовано и сохранит содержимое с описанием типа: "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;
   }

Обратите внимание, что это определение возвращает указатель на текущий объект, в то время как реализация, генерируемая компилятором по умолчанию, имела тип void (это видно из сообщения об ошибке). Это означает, что предоставляемые компилятором по умолчанию операторы присваивания нельзя использовать в цепочке x = y = z. Если вам требуется такая возможность, переопределите operator= явным образом и верните требуемый тип, отличный от void.

 

Указатели

Наиболее практичным является преобразование указателей на объекты разных типов.

В принципе, все варианты приведения указателей объектных типов можно свести к трем:

  • от базового к производному, нисходящее приведение типов (downcast), потому что иерархию классов принято рисовать перевернутым деревом;
  • от производного к базовому, восходящее приведение типов (upcast);
  • между классами разных веток иерархии или вообще из разных семейств.

Последний вариант запрещен (получим ошибку компиляции). Первые два компилятор разрешает, но если "upcast" является естественным и безопасным, то "downcast" способен приводить к ошибкам на стадии выполнения программы.

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

Разумеется, когда используется указатель на объект базового класса, для него нельзя вызвать методы и свойства производного класса, даже если по указателю находится соответствующий объект. Получим ошибку компиляции "неизвестный идентификатор" ("undeclared identifier").

Однако для указателей поддерживается синтаксис явного приведения типов (см. C-стиль), который позволяет в выражениях "на лету" преобразовывать указатель в требуемый тип и разыменовывать его без создания промежуточной переменной.

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

Здесь мы создали объект производного класса (Derived) и указатель на него базового типа (Base *). Для доступа к методу derivedMethod производного класса указатель временно преобразуется к типу Derived.

Тип указателя со звездочкой следует заключать в круглые скобки. Кроме того, само выражение с приведением, включая и имя переменной, также обрамляется еще одной парой круглых скобок.

Другую ошибку компиляции ("несоответствие типов" — "type mismatch") в нашем тесте генерирует строка, где мы пробуем привести указатель на Rectangle к указателю на Circle: они из разных веток наследования.

   c = r// ошибка: type mismatch

Гораздо хуже обстоят дела, когда тип указателя, к которому выполняется приведение, не соответствует реальному объекту (хотя их типы совместимы, и потому программа компилируется нормально). Такая операция закончится ошибкой уже на стадии выполнения программы (то есть компилятор отловить её не может). Программа при этом выгружается.

Например, в скрипте ShapesCasting.mq5 мы описали указатель на Square и присваиваем ему указатель на Shape, в котором находится объект Rectangle.

   Square *s2;
   // ОШИБКА ВРЕМЕНИ ВЫПОЛНЕНИЯ
   s2 = p// ошибка: Incorrect casting of pointers

Терминал выдает ошибку "Неправильное приведение типов указателей" ("Incorrect casting of pointers"). Указатель более конкретного типа Square не способен указывать на родительский объект Rectangle.

Чтобы избежать неприятностей во время выполнения и предотвратить "падение" программы, MQL5 предоставляет специальную языковую конструкцию dynamic_cast. С её помощью можно "осторожно" проверить, можно ли приводить указатель к требуемому типу. Если конвертация возможна, то она будет произведена. А если нет, мы получим нулевой указатель (NULL) и сможем его особым образом обработать (например, с помощью if как-то инициализировать или прервать выполнение функции, но не программы целиком).

Синтаксис dynamic_cast следующий:

dynamic_cast< Класс * >( указатель )

В нашем случае достаточно написать:

   s2 = dynamic_cast<Square *>(p); // пытаемся привести тип, получим NULL при неудаче
   Print(s2); // 0

Программа выполнится нормально.

В частности, можно еще раз попытаться привести прямоугольник к кругу и убедиться, что получим 0:

   c = dynamic_cast<Circle *>(r); // пытаемся привести тип, получим NULL при неудаче
   Print(c); // 0

В MQL5 существует специальный тип указателей, который способен хранить любой объект. Этот тип обозначается: void *.

Продемонстрируем, как переменная void * работает с dynamic_cast.

   void *v;
   v = s;   // устанавливаем на экземпляр 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));

Первые три строки выведут в журнал значение указателя (дескриптор одного и того же объекта), а две последних — 0.

Теперь вернемся к примеру предварительного объявления в разделе Указатели (см. файл ThisCallback.mq5), где классы Manager и Element содержали взаимные указатели.

Тип указателя void * позволяет избавиться от предварительного объявления (ThisCallbackVoid.mq5). Закомментируем строку с ним, а тип поля owner с указателем на объект-менеджер поменяем на void *. В конструкторе также поменяем тип параметра.

// class Manager; 
class Element
{
   void *owner// рассчитываем на совместимость с типом Manager *
public:
   Element(void *t = NULL): owner(t) { } // было Element(Manager &t)
   void doMath()
   {
      const int N = 1000000;
      
      // получаем нужный тип во время выполнения
      Manager *ptr = dynamic_cast<Manager *>(owner);
      // далее везде нужно проверять ptr на NULL перед использованием
      
      for(int i = 0i < N; ++i)
      {
         if(i % (N / 20) == 0)
         {
            if(ptr != NULLptr.progressNotify(&thisi * 100.0f / N);
         }
         // ... lot of calculations
      }
      if(ptr != NULLptr.progressNotify(&this100.0f);
   }
   ...
};

Данный подход может предоставлять большую гибкость, но и требует большей осторожности, так как dynamic_cast способен вернуть NULL. Рекомендуется по возможности пользоваться стандартными средствами диспетчеризации (статической и динамической) с контролем типов, представляемых языком.

Указатели void * обычно становятся необходимы в исключительных случаях. И "лишняя" строка с предварительным описанием — не тот случай. Здесь оно было использовано только как наиболее простой пример универсальности указателя void *.