- Основы ООП: абстракция
- Основы ООП: инкапусляция
- Основы ООП: наследование
- Основы ООП: полиморфизм
- Основы ООП: композиция (дизайн)
- Определение класса
- Права доступа
- Конструкторы: по умолчанию, параметрический, копирования
- Деструкторы
- Ссылка на себя: this
- Наследование
- Динамическое создание объектов: new и delete
- Указатели
- Виртуальные методы (virtual и override)
- Статические члены
- Вложенные типы, пространства имен и оператор контекста '::'
- Разнесение объявления и определения класса
- Абстрактные классы и интерфейсы
- Перегрузка операторов
- Приведение объектных типов: dynamic_cast и указатель void *
- Указатели, ссылки и const
- Управление наследованием: final и delete
Приведение объектных типов: 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&)'
|
Согласно этому сообщению, метод копирования Rectangle::operator=(const Rectangle&) был неявным образом удален компилятором (который и предоставляет его реализацию по умолчанию), поскольку использует аналогичный метод базового класса Shape::operator=(const Shape&), а тот, в свою очередь, был удален из-за наличия поля type с модификатором const. Такие поля можно установить только при создании объекта, и компилятор не знает, как копировать объект при таком ограничении.
Между прочим, эффект "удаления" методов доступен не только компилятору, но прикладному программисту: подробнее об этом будет рассказано в разделе Управление наследованием: final и delete.
Решить проблему можно было бы удалением модификатора const или предоставив собственную реализацию оператора присваивания (в ней const-поле не задействовано и сохранит содержимое с описанием типа: "Rectangle"):
Rectangle *operator=(const Rectangle &r)
|
Обратите внимание, что это определение возвращает указатель на текущий объект, в то время как реализация, генерируемая компилятором по умолчанию, имела тип void (это видно из сообщения об ошибке). Это означает, что предоставляемые компилятором по умолчанию операторы присваивания нельзя использовать в цепочке x = y = z. Если вам требуется такая возможность, переопределите operator= явным образом и верните требуемый тип, отличный от void.
Указатели
Наиболее практичным является преобразование указателей на объекты разных типов.
В принципе, все варианты приведения указателей объектных типов можно свести к трем:
- от базового к производному, нисходящее приведение типов (downcast), потому что иерархию классов принято рисовать перевернутым деревом;
- от производного к базовому, восходящее приведение типов (upcast);
- между классами разных веток иерархии или вообще из разных семейств.
Последний вариант запрещен (получим ошибку компиляции). Первые два компилятор разрешает, но если "upcast" является естественным и безопасным, то "downcast" способен приводить к ошибкам на стадии выполнения программы.
void OnStart()
|
Разумеется, когда используется указатель на объект базового класса, для него нельзя вызвать методы и свойства производного класса, даже если по указателю находится соответствующий объект. Получим ошибку компиляции "неизвестный идентификатор" ("undeclared identifier").
Однако для указателей поддерживается синтаксис явного приведения типов (см. C-стиль), который позволяет в выражениях "на лету" преобразовывать указатель в требуемый тип и разыменовывать его без создания промежуточной переменной.
Base *b;
|
Здесь мы создали объект производного класса (Derived) и указатель на него базового типа (Base *). Для доступа к методу derivedMethod производного класса указатель временно преобразуется к типу Derived.
Тип указателя со звездочкой следует заключать в круглые скобки. Кроме того, само выражение с приведением, включая и имя переменной, также обрамляется еще одной парой круглых скобок.
Другую ошибку компиляции ("несоответствие типов" — "type mismatch") в нашем тесте генерирует строка, где мы пробуем привести указатель на Rectangle к указателю на Circle: они из разных веток наследования.
c = r; // ошибка: type mismatch |
Гораздо хуже обстоят дела, когда тип указателя, к которому выполняется приведение, не соответствует реальному объекту (хотя их типы совместимы, и потому программа компилируется нормально). Такая операция закончится ошибкой уже на стадии выполнения программы (то есть компилятор отловить её не может). Программа при этом выгружается.
Например, в скрипте ShapesCasting.mq5 мы описали указатель на Square и присваиваем ему указатель на Shape, в котором находится объект Rectangle.
Square *s2;
|
Терминал выдает ошибку "Неправильное приведение типов указателей" ("Incorrect casting of pointers"). Указатель более конкретного типа Square не способен указывать на родительский объект Rectangle.
Чтобы избежать неприятностей во время выполнения и предотвратить "падение" программы, MQL5 предоставляет специальную языковую конструкцию dynamic_cast. С её помощью можно "осторожно" проверить, можно ли приводить указатель к требуемому типу. Если конвертация возможна, то она будет произведена. А если нет, мы получим нулевой указатель (NULL) и сможем его особым образом обработать (например, с помощью if как-то инициализировать или прервать выполнение функции, но не программы целиком).
Синтаксис dynamic_cast следующий:
dynamic_cast< Класс * >( указатель ) |
В нашем случае достаточно написать:
s2 = dynamic_cast<Square *>(p); // пытаемся привести тип, получим NULL при неудаче
|
Программа выполнится нормально.
В частности, можно еще раз попытаться привести прямоугольник к кругу и убедиться, что получим 0:
c = dynamic_cast<Circle *>(r); // пытаемся привести тип, получим NULL при неудаче
|
В MQL5 существует специальный тип указателей, который способен хранить любой объект. Этот тип обозначается: void *.
Продемонстрируем, как переменная void * работает с dynamic_cast.
void *v;
|
Первые три строки выведут в журнал значение указателя (дескриптор одного и того же объекта), а две последних — 0.
Теперь вернемся к примеру предварительного объявления в разделе Указатели (см. файл ThisCallback.mq5), где классы Manager и Element содержали взаимные указатели.
Тип указателя void * позволяет избавиться от предварительного объявления (ThisCallbackVoid.mq5). Закомментируем строку с ним, а тип поля owner с указателем на объект-менеджер поменяем на void *. В конструкторе также поменяем тип параметра.
// class Manager;
|
Данный подход может предоставлять большую гибкость, но и требует большей осторожности, так как dynamic_cast способен вернуть NULL. Рекомендуется по возможности пользоваться стандартными средствами диспетчеризации (статической и динамической) с контролем типов, представляемых языком.
Указатели void * обычно становятся необходимы в исключительных случаях. И "лишняя" строка с предварительным описанием — не тот случай. Здесь оно было использовано только как наиболее простой пример универсальности указателя void *.