Особенности встроенных и объектных типов в шаблонах

Следует иметь в виду, что ограничения на применимость типов в шаблоне накладывают 3 важных аспекта:

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

Допустим, у нас описана структура Dummy (см. скрипт TemplatesMax.mq5):

struct Dummy
{
   int x;
};

Если мы попробуем вызвать функцию Max для двух экземпляров структуры, то получим кучу ошибок, определяющие из которых следующие: "объекты можно передавать только по ссылке" и "нельзя применить шаблон".

   // ОШИБКИ:
   // 'object1' - objects are passed by reference only
   // 'Max' - cannot apply template
   Dummy object1object2;
   Max(object1object2);

Суть проблемы заключается в передаче параметров шаблонной функции по значению, а такой способ несовместим с любыми объектными типами. Чтобы её решить, можно поменять тип параметров на ссылки:

template<typename T>
T Max(T &value1T &value2)
{
   return value1 > value2 ? value1 : value2;
}

Прежняя ошибка уйдет, но тогда мы получим новую ошибку: "'>' - неправильное использование оператора" ("'>' - illegal operation use"). Дело в том, что в шаблоне Max есть выражение с операцией сравнения '>'. Следовательно, если в шаблон подставляется пользовательский тип, в нем должен быть перегружен оператор '>' (а в структуре Dummy его нет: скоро мы этим займемся). Для более сложных функций, скорее всего, потребуется перегрузить гораздо большее число операторов. К счастью, компилятор подсказывает, чего именно не хватает.

Однако смена способа передачи параметров функции по ссылке дополнительно привела к неработоспособности предыдущего вызова:

Print(Max<ulong>(100010000000));

Теперь он генерирует ошибки: "параметр передается по ссылке, ожидается переменная" ("parameter passed as reference, variable expected"). Таким образом, наш шаблон функции перестал работать с литералами и прочими временными значениями (в частности, в него нельзя напрямую передать выражение или результат вызова другой функции).

Можно было бы подумать, что универсальным выходом из ситуации стала бы перегрузка шаблонной функции, то есть определение обоих вариантов, отличающихся лишь амперсандом в параметрах:

template<typename T>
T Max(T &value1T &value2)
{
   return value1 > value2 ? value1 : value2;
}
  
template<typename T>
T Max(T value1T value2)
{
   return value1 > value2 ? value1 : value2;
}

Но это не сработает. Теперь компилятор выдает ошибку "неоднозначная перегрузка функции с одинаковыми параметрами":

'Max' - ambiguous call to overloaded function with the same parameters
could be one of 2 function(s)
   T Max(T&,T&)
   T Max(T,T)

Окончательный, рабочий вариант перегрузки потребует добавить модификатор const для ссылок. Попутно мы добавили в шаблон Max оператор Print, чтобы видеть в журнале, какая из перегруженных версий вызывается и какому типу параметров соответствует T.

template<typename T>
T Max(const T &value1const T &value2)
{
   Print(__FUNCSIG__" T="typename(T));
   return value1 > value2 ? value1 : value2;
}
   
template<typename T>
T Max(T value1T value2)
{
   Print(__FUNCSIG__" T="typename(T));
   return value1 > value2 ? value1 : value2;
}
   
struct Dummy
{
   int x;
   bool operator>(const Dummy &otherconst
   {
      return x > other.x;
   }
};

Также мы реализовали перегрузку оператора '>' в структуре Dummy. Поэтому все вызовы функции Max в тестовом скрипте завершаются успешно: как для встроенных типов, так и пользовательских, а также для литералов и переменных. В журнал выводится:

double Max<double>(double,double) T=double
1.0
datetime Max<datetime>(datetime,datetime) T=datetime
2021.10.10 00:00:00
ulong OnStart::Max<ulong>(ulong,ulong) T=ulong
10000000
Dummy Max<Dummy>(const Dummy&,const Dummy&) T=Dummy

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

template<typename T>
T Max(T value1T value2)
{
   // вызываем функцию с параметрами по ссылке
   return Max(value1value2true);
}
   
template<typename T>
T Max(const T &value1const T &value2const bool ref = false)
{
   return (T)(value1 > value2 ? value1 : value2);
}

Нам осталось рассмотреть еще один нюанс, связанный с пользовательскими типами, а именно использование в шаблонах указателей (напомним, они применимы только к объектам классов). Создадим простой класс Data и попробуем вызвать шаблонную функцию Max для указателей на его объекты.

class Data
{
public:
   int x;
   bool operator>(const Data &otherconst
   {
      return x > other.x;
   }
};
   
void OnStart()
{
   ... 
   Data *pointer1 = new Data();
   Data *pointer2 = new Data();
   Max(pointer1pointer2);
   delete pointer1;
   delete pointer2;
}

Мы увидим в журнале, что 'T=Data*', то есть атрибут указателя попадает в подставленный тип. Это наводит на мысль, что при необходимости можно описать еще одну перегрузку шаблонной функции, которая будет ответственна только за указатели.

template<typename T>
T *Max(T *value1T *value2)
{
   Print(__FUNCSIG__" T="typename(T));
   return value1 > value2 ? value1 : value2;
}

В этом случае атрибут указателя '*' уже присутствует в параметрах шаблона, и потому выведение типа приводит к 'T=Data'. Такой подход позволяет предоставлять отдельную реализацию шаблона для указателей.

При наличии нескольких шаблонов, которые подходят для генерации экземпляра с конкретными типами, выбирается наиболее специализированная версия шаблона. В частности, при вызове функции Max с аргументами-указателями подойдут два шаблона с параметрами T (T=Data*) и T* (T=Data), но поскольку первый способен принимать и значения, и указатели, то он является более общим, чем второй, который работает только с указателями. Поэтому для указателей будет выбран второй. Иными словами, чем меньше модификаторов в актуальном типе, который подставляется на место T, тем предпочтительнее вариант шаблона. Помимо атрибута указателя '*' сюда относится и модификатор const. Параметры const T* или const T — более специализированы, чем просто T* или T, соответственно.