Шаблоны методов

Шаблоном может быть не только объектный тип целиком, но и его метод в отдельности — простой или статический. Исключение составляют виртуальные методы: их нельзя делать шаблонами. Отсюда следует, что шаблонные методы нельзя описывать внутри интерфейсов. Вместе с тем, сами интерфейсы можно делать шаблонами, и виртуальные методы могут присутствовать в шаблонах классов.

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

Шаблон метода описывается аналогично шаблону функции, но только в контексте класса, структуры или объединения (которые могут быть или не быть шаблонами).

template < typename T[typename Ti ...] > ]
class имя_класса
{
   ...
   template < typename U [, typename Ui ...] >
   тип имя_метода(параметры_с_типами_T_и_U)
   {
   }
};

В параметрах, возвращаемом значении и теле метода могут использоваться типы T (общие для класса) и U (специфические для метода).

Экземпляр метода для конкретного сочетания параметров генерируется только при обращении к нему в коде программы.

В предыдущем разделе мы описали шаблонный класс AutoPtr для хранения и освобождения одного указателя. Когда однотипных указателей много, их удобно складывать в объект-контейнер. Создадим простой шаблон с подобным функционалом — класс SimpleArray (SimpleArray.mqh). Чтобы не дублировать функционал по контролю за освобождением динамической памяти, заложим в контракт класса, что он предназначен для хранения значений и объектов, но не указателей. Для хранения указателей будем помещать их в объекты AutoPtr, а уже те — в контейнер.

Это имеет еще один положительный эффект: поскольку объект AutoPtr маленький, его легко копировать (без расхода ресурсов), что часто происходит при обмене данными между функциями. А объекты тех прикладных классов, на которые AutoPtr указывает, могут быть большими, и в них даже не обязательно реализовывать свой конструктор копирования.

Конечно, дешевле всего возвращать из функций сами указатели, но тогда потребуется заново изобретать средства контроля за освобождением памяти. Поэтому проще воспользоваться готовым решением в виде AutoPtr.

Для объектов внутри контейнера заведем массив data шаблонизированного типа T.

template<typename T>
class SimpleArray
{
protected:
   T data[];
   ...

Поскольку одна из основных операций для контейнера — добавление элемента, предусмотрим вспомогательную функцию для расширения массива.

   int expand()
   {
      const int n = ArraySize(data);
      ArrayResize(datan + 1);
      return n;
   }

Непосредственно добавление элементов будем делать через перегруженный оператор '<<'. Он использует общий параметр T шаблона.

public:
   SimpleArray *operator<<(const T &r)
   {
      data[expand()] = (T)r;
      return &this;
   }

Данный вариант принимает значение по ссылке, то есть переменную или объект. Следует пока обратить на это внимание, а почему это важно — станет ясно через пару моментов.

Чтение элементов осуществим путем перегрузки оператора '[]' (он имеет наивысший приоритет и потому не потребует использовать круглые скобки в выражениях).

   T operator[](int iconst
   {
      return data[i];
   }

Сперва убедимся в работоспособности класса на примере структуры.

struct Properties
{
   int x;
   string s;
};

Для этого опишем в функции OnStart контейнер для структуры и поместим в него один объект (TemplatesSimpleArray.mq5).

void OnStart()
{
   SimpleArray<PropertiesarrayStructs;
   Properties prop = {12345"abc"};
   arrayStructs << prop;
   Print(arrayStructs[0].x" "arrayStructs[0].s);
   ...
}

Отладочный вывод в журнал позволяет убедиться, что структура находится в контейнере.

Теперь попробуем сохранить в контейнер несколько чисел.

   SimpleArray<doublearrayNumbers;
   arrayNumbers << 1.0 << 2.0 << 3.0;

К сожалению, мы получим ошибки "параметр передается по ссылке, требуется переменная" ("parameter passed as reference, variable expected"), которые возникают как раз в перегруженном операторе '<<'.

Нам требуется перегрузка с передачей параметра по значению. Однако мы не можем просто написать похожий метод, который отличается отсутствием const и '&':

   SimpleArray *operator<<(T r)
   {
      data[expand()] = (T)r;
      return &this;
   }

Если так сделать, новый вариант приведет к некомпилируемости шаблона для объектных типов: ведь объекты нужно передавать только по ссылке. Даже если функция не используется для объектов, она все равно присутствует в классе. Поэтому мы определим новый метод как шаблон с собственным параметром.

template<typename T>
class SimpleArray
{
   ...
   template<typename U>
   SimpleArray *operator<<(U u)
   {
      data[expand()] = (T)u;
      return &this;
   }

Он появится в классе только в том случае, если в оператор '<<' передается что-то по значению, а значит это точно не объект. Правда, мы не можем гарантировать, что T и U совпадают, поэтому выполняется явное приведение типа (T)u. Для встроенных типов (если два типа не совпадут) в некоторых сочетаниях возможна конверсия с потерей точности, но код точно скомпилируется. Единственное исключение — запрет на преобразование строки в логический тип, но вряд ли контейнер станут использовать для массива bool, поэтому данное ограничение не существенно. Желающие могут решить эту проблему.

С новым шаблонным методом контейнер SimpleArray<double> работает как ожидалось и не конфликтует с SimpleArray<Properties>, так как у этих двух экземпляров шаблонов есть отличия в сгенерированном исходном коде.

Наконец, проверим контейнер с объектами AutoPtr. Для этого подготовим простой класс Dummy, который будет "поставлять" объекты для указателей внутри AutoPtr.

class Dummy
{
   int x;
public:
   Dummy(int i) : x(i) { }
   int value() const
   {
      return x;
   }
};

В функции OnStart создадим контейнер SimpleArray<AutoPtr<Dummy>> и заполним его.

void OnStart()
{
   SimpleArray<AutoPtr<Dummy>> arrayObjects;
   AutoPtr<Dummyptr = new Dummy(20);
   arrayObjects << ptr;
   arrayObjects << AutoPtr<Dummy>(new Dummy(30));
   Print(arrayObjects[0][].value());
   Print(arrayObjects[1][].value());
}

Напомним, что в AutoPtr оператор '[]' используется для возврата хранимого указателя, поэтому запись arrayObjects[0][] означает: вернуть 0-й элемент массива data в SimpleArray, то есть объект AutoPtr, и затем к тому применяется вторая пара квадратных скобок, что в результате дает указатель Dummy*. Далее мы можем работать со всеми свойствами этого объекта: в данном случае, мы извлекаем текущее значение поля x.

Поскольку в Dummy нет конструктора копирования, нельзя использовать контейнер для хранения этих объектов напрямую, без AutoPtr.

   // ОШИБКА:
   // object of 'Dummy' cannot be returned,
   // copy constructor 'Dummy::Dummy(const Dummy &)' not found
   SimpleArray<Dummybad;

Но находчивый пользователь может догадаться, как это обойти.

   SimpleArray<Dummy*> bad;
   bad << new Dummy(0);

Такой код будет компилироваться и работать. Однако в этом "решении" содержится проблема: SimpleArray не умеет контролировать указатели, и потому при выходе из программы обнаружится утечка памяти.

1 undeleted objects left
1 object of type Dummy left
24 bytes of leaked memory

Мы, как разработчики SimpleArray, обязаны прикрыть эту лазейку. Для этого добавим в класс еще один шаблонный метод с перегрузкой оператора '<<' — на этот раз для указателей. Поскольку это шаблон, он также включается в результирующий исходный код только "по требованию": когда программист пытается использовать данную перегрузку, то есть записать в контейнер указатель. В остальных случаях метод игнорируется.

template<typename T>
class SimpleArray
{
   ...
   template<typename P>
   SimpleArray *operator<<(P *p)
   {
      data[expand()] = (T)*p;
      if(CheckPointer(p) == POINTER_DYNAMICdelete p;
      return &this;
   }

Данная специализация вызывает ошибку компиляции ("object pointer expected") при создании экземпляра шаблона с типом указателя. Тем самым мы сообщаем пользователю, что такой режим не поддерживается.

   SimpleArray<Dummy*> bad// ОШИБКА генерируется в SimpleArray.mqh

Кроме того, она выполняет еще одно защитное действие. Если в клиентском классе всё же будет конструктор копирования, то сохранение динамически распределенных объектов в контейнере перестанет приводить к утечке памяти: копия объекта по переданному указателю P *p остается в контейнере, а оригинал — удаляется. При уничтожении контейнера в конце функции OnStart, его внутренний массив data автоматически вызовет деструкторы для своих элементов.

void OnStart()
{
   ...
   SimpleArray<Dummygood;
   good << new Dummy(0);
// SimpleArray "чистит" свои элементы
 // нет забытых объектов в памяти

Шаблоны методов и "простые" методы могут быть определены вне основного блока класса (или шаблона класса), по аналогии с тем, что мы рассматривали в разделе Разнесение объявления и определения класса. При этом они все предваряются заголовком шаблона (TemplatesExtended.mq5):

template<typename T>
class ClassType
{
   ClassType() // закрытый конструктор
   {
      s = &this;
   }
   static ClassType *s// указатель на объект (если был create)
public:
   static ClassType *create() // создание (только при первом вызове)
   {
      static ClassType single// паттерн "одиночка" для каждого T
      return single;
   }
 
   static ClassType *check() // проверка указателя без создания
   {
      return s;
   }
   
   template<typename U>
   void method(const U &u);
};
   
template<typename T>
template<typename U>
void ClassType::method(const U &u)
{
   Print(__FUNCSIG__" "typename(T), " "typename(U));
}
   
template<typename T>
static ClassType<T> *ClassType::s = NULL;

Здесь также показана инициализация шаблонизированной статической переменной, обозначающий паттерн проектирования "одиночка" (singleton).

В функции OnStart создадим экземпляр шаблона и протестируем его:

void OnStart()
{
   ClassType<string> *object = ClassType<string>::create();
   double d = 5.0;
   object.method(d);
   // ВЫВОД:
   // void ClassType<string>::method<double>(const double&) string double
   
   Print(ClassType<string>::check()); // 1048576 (пример id экземпляра)
   Print(ClassType<long>::check());   // 0 (нет экземпляра для T=long)
}