Шаблоны объектных типов
Определение шаблона объектного типа начинается с заголовка, содержащего типизированные параметры (см. раздел Заголовок шаблона), и привычного определения класса, структуры или объединения.
template < typename T [, typename Ti ...] >
|
Единственное отличие от стандартного определения заключается в том, что параметры шаблона могут встречаться в блоке кода, во всех синтаксических конструкциях языка, где допустимо использовать название типа.
После того как шаблон определен, его рабочие экземпляры создаются при описании в коде переменных шаблонного типа с указанием конкретных типов в угловых скобках:
ClassName<Type1,Type2> object;
|
В отличие от вызова шаблонных функций, компилятор не способен вывести актуальные типы для шаблонов объектов самостоятельно.
Описание переменной шаблонного класса/структуры — не единственный способ создать экземпляр шаблона. Экземпляр также генерируется компилятором, если шаблонный тип используется в качестве базового для другого — конкретного (нешаблонного) класса или структуры.
Например, следующий класс Worker, даже будучи пустым, представляет собой реализацию Base для типа double:
class Worker : Base<double>
|
Такого минимального определения достаточно (с поправкой на добавление конструкторов, если их требует класс Base), чтобы запустить компиляцию и проверку кода шаблона.
В разделе Динамическое создание объектов мы познакомились с понятием динамического указателя на объект, получаемого с помощью оператора new. Этот гибкий механизм имеет один недостаток: за указателями нужно следить и не забывать удалить "вручную", когда они становятся не нужны. В частности, при выходе из функции или блока кода все локальные указатели нужно очистить с помощью вызова delete.
Чтобы упростить решение данной задачи, создадим шаблонный класс AutoPtr (TemplatesAutoPtr.mq5, AutoPtr.mqh). Его параметр T используем для описания поля ptr, хранящего указатель на объект произвольного класса. Значение указателя будем получать через параметр конструктора (T *p) или в перегруженном операторе '='. Основную работу поручим деструктору: в нем указатель будет удаляться вместе с объектом AutoPtr (для этого выделен статический вспомогательный метод free).
Принцип работы AutoPtr простой: локальный объект данного класса будет автоматически уничтожаться при выходе из блока, где он описан, и если ему до этого было поручено "следить" за каким-либо указателем, то AutoPtr освободит и его.
template<typename T>
|
Дополнительно в классе AutoPtr реализован конструктор копирования (а точнее — перемещения, т.к. хозяином указателя становится текущий объект), что позволяет возвращать экземпляр AutoPtr вместе с контролируемым указателем из какой-либо функции.
Для проверки работоспособности AutoPtr опишем фиктивный класс Dummy.
class Dummy
|
В скрипте, в функции OnStart введем переменную AutoPtr<Dummy> и получим для неё значение из функции generator. В самой функции generator также опишем объект AutoPtr<Dummy> и последовательно создадим и "привяжем" к нему два динамических объекта Dummy (чтобы проверить корректное освобождение памяти от "старого" объекта).
AutoPtr<Dummy> generator()
|
Поскольку во всех основных методах стоит вывод в журнал дескрипторов объектов (как самих AutoPtr, так и контролируемых указателей ptr), мы можем отследить все "превращения" указателей (для удобства все строки пронумерованы).
01 Dummy::Dummy(int) 3145728
|
Отвлечемся ненадолго от шаблонов и подробно опишем работу утилиты, потому что подобный класс может пригодиться многим.
Сразу после запуска OnStart происходит вызов функции generator. Она должна вернуть значение для инициализации объекта AutoPtr в OnStart, и потому его конструктор пока не вызывается. В строке 02 создается объект AutoPtr#2097152 внутри функции generator и получает указатель на первый Dummy#3145728. Далее создается второй экземпляр Dummy#4194304 (строка 03), который заменяет в AutoPtr#2097152 прежнюю копию с дескриптором 3145728 (строка 04), причем прежняя копия удаляется (строка 05). В строке 06 создается временный объект AutoPtr#5242880 для возврата значения из generator, а локальный — удаляется (07). В строке 08 наконец используется конструктор копирования для объекта AutoPtr#1048576 в функции OnStart, и в него переносится указатель из временного объекта (который тут же удаляется в строке 09). Далее мы вызываем Print с содержимым указателя. По завершении OnStart автоматически срабатывает деструктор AutoPtr (11), в результате чего мы также удаляем рабочий объект Dummy (12).
Технология шаблонов делает класс AutoPtr параметризованным менеджером динамически распределяемых объектов. Но поскольку AutoPtr имеет поле T *ptr, он применим только для классов (точнее, указателей на объекты классов). Например, попытка инстанцировать шаблон для строки (AutoPtr<string> s) приведет к множеству ошибок в тексте шаблона, смысл которых в том, что тип string не поддерживает указатели.
Здесь это не является проблемой, поскольку назначение данного шаблона ограничено классами, однако для более универсальных шаблонов этот нюанс следует иметь в виду (см. врезку).
Указатели и ссылки
Обратите внимание, что конструкция T * не может встречаться в шаблонах, которые планируется применять, в том числе, для встроенных типов или структур. Дело в том, что указатели в MQL5 разрешены только для классов. Это не означает, что шаблон в принципе не может быть написан так, чтобы применяться и для встроенных, и для пользовательских типов, однако это может потребовать некоторых ухищрений. Вероятно, потребуется либо отказаться от части функционала, либо поступиться уровнем универсальности шаблона (сделать несколько шаблонов вместо одного, перегрузить функции и т.д.).
Наиболее прямолинейный способ "внедрения" в шаблон типа-указателя — включать модификатор '*' вместе с актуальным типом при создании экземпляра шаблона (то есть должно выполняться соответствие T=Type*). Вместе с тем, некоторые функции (такие как CheckPointer), операторы (например, delete) и синтаксические конструкции (например, приведение типа ((T)variable)), чувствительны к тому, являются ли их аргументы/операнды указателями или нет. В связи с этим, один и тот же текст шаблона не всегда синтаксически корректен одновременно и для указателей, и для значений простых типов.
Еще одно существенное различие в типах, которое следует иметь в виду: объекты передаются в методы только по ссылке, но литералы (константы) простых типов не могут передаваться по ссылке. Из-за этого наличие или отсутствие амперсанда может расцениваться компилятором, как ошибка, в зависимости от выведенного типа T. В качестве одного из "обходных маневров" можно, при необходимости, "оборачивать" константы-аргументы в объекты или переменные.
Другой прием связан с использованием шаблонных методов: он показан в следующем разделе.
Следует отметить, что объектно-ориентированные методики хорошо сочетаются с шаблонами. Поскольку указатель на базовый класс можно использовать для хранения объекта производного класса, AutoPtr применим для объектов любых классов-наследников Dummy.
В принципе, данный "гибридный" подход широко используется в классах-контейнерах (вектор, очередь, карта, список и т.д.), которые, как правило, являются шаблонными. Классы-контейнеры могут, в зависимости от реализации, накладывать на параметр шаблона дополнительные требования, в частности, чтобы подставляемый тип имел конструктор копирования и оператор присваивания (копирования).
В стандартной библиотеке MQL5, поставляемой вместе с MetaTrader 5, имеется множество готовых шаблонов из этой серии: Stack.mqh, Queue.mqh, HashMap.mqh, LinkedList.mqh, RedBlackTree.mqh и другие — все они находятся в каталоге MQL5/Include/Generic. Правда, они не обеспечивают контроль за динамическими объектами (указателями).
Мы рассмотрим собственный пример простого класса-контейнера в разделе Шаблоны методов.