Шаблоны объектных типов

Определение шаблона объектного типа начинается с заголовка, содержащего типизированные параметры (см. раздел Заголовок шаблона), и привычного определения класса, структуры или объединения.

template < typename T [, typename Ti ...] >
class имя_класса
{
   ...
};

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

После того как шаблон определен, его рабочие экземпляры создаются при описании в коде переменных шаблонного типа с указанием конкретных типов в угловых скобках:

ClassName<Type1,Type2object;
StructName<Type1,Type2,Type3struct;
ClassName<Type1,Type2> *pointer = new ClassName<Type1,Type2>();
ClassName1<ClassName2<Type>> 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>
class AutoPtr
{
private:
   T *ptr;
   
public:
   AutoPtr() : ptr(NULL) { }
   
   AutoPtr(T *p) : ptr(p)
   {
      Print(__FUNCSIG__" ", &this": "ptr);
   }
   
   AutoPtr(AutoPtr &p)
   {
      Print(__FUNCSIG__" ", &this": "ptr" -> "p.ptr);
      free(ptr);
      ptr = p.ptr;
      p.ptr = NULL;
   }
   
   ~AutoPtr()
   {
      Print(__FUNCSIG__" ", &this": "ptr);
      free(ptr);
   }
   
   T *operator=(T *n)
   {
      Print(__FUNCSIG__" ", &this": "ptr" -> "n);
      free(ptr);
      ptr = n;
      return ptr;
   }
   
   Toperator[](int x = 0const
   {
      return ptr;
   }
   
   static void free(void *p)
   {
      if(CheckPointer(p) == POINTER_DYNAMICdelete p;
   }
};

Дополнительно в классе AutoPtr реализован конструктор копирования (а точнее — перемещения, т.к. хозяином указателя становится текущий объект), что позволяет возвращать экземпляр AutoPtr вместе с контролируемым указателем из какой-либо функции.

Для проверки работоспособности AutoPtr опишем фиктивный класс Dummy.

class Dummy
{
   int x;
public:
   Dummy(int i) : x(i)
   {
      Print(__FUNCSIG__" ", &this);
   }
   ...
   int value() const
   {
      return x;
   }
};

В скрипте, в функции OnStart введем переменную AutoPtr<Dummy> и получим для неё значение из функции generator. В самой функции generator также опишем объект AutoPtr<Dummy> и последовательно создадим и "привяжем" к нему два динамических объекта Dummy (чтобы проверить корректное освобождение памяти от "старого" объекта).

AutoPtr<Dummygenerator()
{
   AutoPtr<Dummyptr(new Dummy(1));
   // указатель на 1 будет освобожден при выполнении '='
   ptr = new Dummy(2);
   return ptr;
}
   
void OnStart()
{
   AutoPtr<Dummyptr = generator();
   Print(ptr[].value());             // 2
}

Поскольку во всех основных методах стоит вывод в журнал дескрипторов объектов (как самих AutoPtr, так и контролируемых указателей ptr), мы можем отследить все "превращения" указателей (для удобства все строки пронумерованы).

01 Dummy::Dummy(int) 3145728
02  AutoPtr<Dummy>::AutoPtr<Dummy>(Dummy*) 2097152: 3145728
03  Dummy::Dummy(int) 4194304
04  Dummy*AutoPtr<Dummy>::operator=(Dummy*) 2097152: 3145728 -> 4194304
05  Dummy::~Dummy() 3145728
06  AutoPtr<Dummy>::AutoPtr<Dummy>(AutoPtr<Dummy>&) 5242880: 0 -> 4194304
07  AutoPtr<Dummy>::~AutoPtr<Dummy>() 2097152: 0
08  AutoPtr<Dummy>::AutoPtr<Dummy>(AutoPtr<Dummy>&) 1048576: 0 -> 4194304
09  AutoPtr<Dummy>::~AutoPtr<Dummy>() 5242880: 0
10  2
11  AutoPtr<Dummy>::~AutoPtr<Dummy>() 1048576: 4194304
12  Dummy::~Dummy() 4194304

Отвлечемся ненадолго от шаблонов и подробно опишем работу утилиты, потому что подобный класс может пригодиться многим.
 
Сразу после запуска 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. Правда, они не обеспечивают контроль за динамическими объектами (указателями).

Мы рассмотрим собственный пример простого класса-контейнера в разделе Шаблоны методов.