Шаблоны функций

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

В качестве первого примера рассмотрим функцию Swap для обмена местами двух элементов массива (TemplatesSorting.mq5). Параметр шаблона T используется как тип входной переменной-массива, а также как тип локальной переменной temp.

template<typename T>
void Swap(T &array[], const int iconst int j)
{
   const T temp = array[i];
   array[i] = array[j];
   array[j] = temp;
}

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

Компилятор по умолчанию генерирует реализацию оператора копирования для классов и структур, но она может быть удалена неявно или явно (см. ключевое слово delete). В частности, как мы видели в разделе Приведение объектных типов, наличие в классе константного поля приводит к тому, что компилятор удаляет свой неявный вариант копирования. Тогда вышеприведенная шаблонная функция Swap не сможет быть использована для объектов такого класса: компилятор выдаст ошибку.

Для классов/структур, с которыми работает функция Swap, желательно иметь не только оператор присваивания, но и конструктор копирования, потому что описание переменной temp на самом деле является конструированием с инициализацией, а не присваиванием. С конструктором копирования первая строка функции выполняется за один прием (temp создается на основе array[i]), в то время как в его отсутствие сначала будет вызван конструктор по умолчанию, а затем для temp выполнится оператор '='.

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

template<typename T>
void QuickSort(T &array[], const int start = 0int end = INT_MAX)
{
   if(end == INT_MAX)
   {
      end = start + ArraySize(array) - 1;
   }
   if(start < end)
   {
      int pivot = start;
      
      for(int i = starti <= endi++)
      {
         if(!(array[i] > array[end]))
         {
            Swap(arrayipivot++);
         }
      }
      
      --pivot;
   
      QuickSort(arraystartpivot - 1);
      QuickSort(arraypivot + 1end);
   }
}

Обратите внимание, что параметр T шаблона QuickSort определяет тип входного параметра array, и этот массив затем передается в шаблон Swap. Таким образом, выведение типа T для шаблона QuickSort автоматически определит и тип T для шаблона Swap.

Встроенная функция ArraySize (как и многие другие) способна работать с массивами произвольных типов: в некотором смысле она также является шаблонной, хотя и реализована непосредственно в терминале.

Сортировка выполняется благодаря оператору сравнения '>' в инструкции if. Как мы уже отметили ранее, данный оператор должен быть определен для любого типа T, для которого выполняется сортировка, потому что он применяется к элементам массива типа T.

Проверим, как сортировка работает для массивов встроенных типов.

void OnStart()
{
   double numbers[] = {3411, -74915, -10011};
   QuickSort(numbers);
   ArrayPrint(numbers);
   // -100.00000 -7.00000 11.00000 11.00000 15.00000 34.00000 49.00000
   
   string messages[] = {"usd""eur""jpy""gbp""chf""cad""aud""nzd"};
   QuickSort(messages);
   ArrayPrint(messages);
   // "aud" "cad" "chf" "eur" "gbp" "jpy" "nzd" "usd"
}

Два вызова шаблонной функции QuickSort приводят к автоматическому выведению типа T на основе типов передаваемых массивов. В результате мы получим два экземпляра QuickSort для типов double и string.

Для проверки сортировки пользовательского типа создадим структуру ABC с целочисленным полем x, и будем его заполнять случайными числами в конструкторе. В структуре также важно перегрузить оператор '>'.

struct ABC
{
   int x;
   ABC()
   {
      x = rand();
   }
   bool operator>(const ABC &otherconst
   {
      return x > other.x;
   }
};
void OnStart()
{
   ...
   ABC abc[10];
   QuickSort(abc);
   ArrayPrint(abc);
   /* Пример вывода:
            [x]
      [0]  1210
      [1]  2458
      [210816
      [313148
      [415393
      [520788
      [624225
      [729919
      [832309
      [932589
   */
}

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

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

Например, следующая шаблонная функция createInstance требует явного указания типа в инструкции вызова, так как автоматически "вычислить" тип T по возвращаемому значению нельзя. Если этого не сделать, компилятор генерирует ошибку "несоответствие шаблона" ("template mismatch").

class Base
{
   ...
};
   
template<typename T>
T *createInstance()
{
   T *object = new T(); // вызов конструктора
   ...                  // настройка объекта
   return object
}
   
void OnStart()
{
   Base *p1 = createInstance();       // ошибка: template mismatch
   Base *p2 = createInstance<Base>(); // ok, явное указание
   ...
}

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

template<typename T,typename U>
T MyCast(const U u)
{
   return (T)u;
}
   
void OnStart()
{
   double d = MyCast<double,string>("123.0");
   string f = MyCast<string,double>(123.0);
}

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

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

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

Нешаблонные перегрузки имеют приоритет перед шаблонными, из шаблонных "выигрывают" более специализированные ("узконаправленные").

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

Если несколько вариантов функции подходит в равной степени, мы получим ошибку "неоднозначный вызов перегруженной функции с одинаковыми параметрами" ("ambiguous call to overloaded function with the same parameters").

Например, если в дополнение к шаблону MyCast определена функция для преобразования строки в логический тип:

bool MyCast(const string u)
{
   return u == "true";
}

то вызов MyCast<double,string>("123.0") начнет порождать указанную ошибку, потому что две функции различаются только возвращаемым значением:

'MyCast<double,string>' - ambiguous call to overloaded function with the same parameters
could be one of 2 function(s)
   double MyCast<double,string>(const string)
   bool MyCast(const string)

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

Если функция имеет параметр шаблонизированного типа T со значением по умолчанию, и при вызове соответствующий аргумент будет опущен, то компилятор также не сможет вывести тип T и выдаст ошибку "нельзя применить шаблон" ("cannot to apply template").

class Base
{
public:
   Base(const Base *source = NULL) { }
   static Base *type;
};
   
static BaseBase::type;
   
template<typename T>
T *createInstanceFrom(T *origin = NULL)
{
   T *object = new T(origin);
   return object
}
   
void OnStart()
{
   Base *p1 = createInstanceFrom();   // ошибка: cannot to apply template
   Base *p2 = createInstanceFrom(Base::type); // ok, авто-определение из аргумента
   Base *p3 = createInstanceFrom<Base>();     // ok, явное указание, аргумент опущен
}