Общие принципы работы шаблонов

Вспомним перегрузку функций. Она заключается в определении нескольких версий функции с отличающимися параметрами, в том числе, если количество параметров одинаковое, но их типы — разные. Зачастую алгоритм таких функций — один и тот же для параметров разных типов. Например, в MQL5 имеется встроенная функция MathMax, которая возвращает наибольшее из двух переданных в неё значений:

double MathMax(double value1double value2);

Несмотря на то, что прототип предоставлен только для типа double, функция на самом деле способна работать с парами аргументов других числовых типов, например, int или datetime. Иными словами, функция является перегруженной ядром для встроенных числовых типов. Если бы мы хотели достичь того же эффекта в своем исходном коде, нам пришлось бы перегрузить функцию за счет дублирования с разными параметрами, например, так:

double Max(double value1double value2)
{
   return value1 > value2 ? value1 : value2;
}
 
int Max(int value1int value2)
{
   return value1 > value2 ? value1 : value2;
}
 
datetime Max(datetime value1datetime value2)
{
   return value1 > value2 ? value1 : value2;
}

Все реализации (тела функций) одинаковы. Меняются только типы параметров.

В таких случаях и применяются шаблоны. С помощью них мы можем описать один образец алгоритма с требуемой реализацией, а компилятор сам сгенерирует по нему несколько экземпляров для конкретных типов, задействованных в программе. Генерация происходит на лету в процессе компиляции и незаметна для программиста (если только в шаблоне не допущена ошибка). Полученный автоматическим способом исходный код не вставляется в текст программы, а напрямую преобразуется в двоичный код (ex5-файл).

В шаблоне в качестве одного или нескольких параметров вводятся формальные обозначения типов, для которых на стадии компиляции по особым правилам выведения типов будут подобраны реальные типы из числа встроенных или пользовательских. Например, функцию Max можно было бы описать следующим шаблоном с параметром-типом T:

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

И далее — применять его для переменных различных типов (см. TemplatesMax.mq5):

void OnStart()
{
   double d1 = 0d2 = 1;
   datetime t1 = D'2020.01.01', t2 = D'2021.10.10';
   Print(Max(d1d2));
   Print(Max(t1t2));
   ...
}

В данном случае компилятор автоматически сгенерирует варианты функции Max для типов double и datetime.

Шаблон сам по себе не генерирует исходный код. Для этого необходимо тем или иным образом создать экземпляр шаблона: вызвать шаблонную функцию или упомянуть имя шаблонного класса с конкретными типами для создания объекта или класса-наследника.

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

template<typename T>
void function()
{
   это не комментарий, но и не исходный код
   !%^&*
}

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

Так, в приведенном примере функции Max, мы вызвали шаблонную функцию два раза: для пары переменных типа double, и для пары переменных типа datetime. В результате было получено два экземпляра функции Max с исходным кодом для соответствий T=double и T=datetime. Разумеется, если этот же шаблон будет вызываться в других частях кода для тех же типов, новые экземпляры генерироваться не будут. Новый экземпляр шаблона потребуется только в случае применения шаблона для другого типа (или набора типов, если параметров больше 1).

Обратите внимание, что параметр у шаблона Max один, и он задает тип сразу для двух входных параметров функции и её возвращаемого значения. Иными словами, описание шаблона способно накладывать определенные ограничение на типы допустимых аргументов.

Если бы мы вызвали Max для переменных разных типов, компилятор не смог бы выявить тип для создания экземпляра шаблона и выдал бы ошибку "неоднозначные параметры шаблона, должно быть 'double' или 'datetime'":

Print(Max(d1t1)); // template parameter ambiguous,
                    // could be 'double' or 'datetime'

Данный процесс выявления реальных типов для параметров шаблона на основании контекста использования шаблона называется выведением (deduction) типов. В MQL5 выведение типов доступно только для шаблонов функций и методов.

Для классов, структур и объединений используется другой способ привязки типов к параметрам шаблона: требуемые типы явно указываются при создании экземпляра шаблона в угловых скобках (если параметров несколько, то соответствующее количество типов указывается через запятую). Подробнее об этом — в разделе про шаблоны объектных типов.

Этот же явный способ можно применять и для функций, как альтернативу для автоматического выведения типов.

Например, мы можем сгенерировать и вызвать экземпляр Max для типа ulong:

Print(Max<ulong>(100010000000));

В данном случае, если бы не явное указание, шаблонная функция была бы связана с типом int (на основании значений целочисленных констант).