8.14 – Создание экземпляра шаблона функции

Добавлено 27 июня 2021 в 12:58

В предыдущем уроке (8.13 – Шаблоны функций) мы представили шаблоны функций и преобразовали обычную функцию max() в шаблон функции max<T>:

template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}

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

Использование шаблона функции

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

Чтобы использовать наш шаблон функции max<T>, мы можем вызвать функцию со следующим синтаксисом:

max<фактический_тип>(arg1, arg2); // фактический_тип - это какой-то фактический тип,
                                  // например, int или double

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

Давайте посмотрим на это на простом примере:

#include <iostream>
 
template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max<int>(1, 2) << '\n'; // создает и вызывает функцию max<int>(int, int)
 
    return 0;
}

Когда компилятор встречает вызов функции max<int>(1, 2), он определяет, что определение функции для max<int>(int, int) еще не существует. Следовательно, для его создания компилятор будет использовать наш шаблон функции max<T>.

Процесс создания функций (с определенными типами) из шаблонов функций (с шаблонными типами) называется созданием экземпляра шаблона функции (или для краткости созданием экземпляра). Когда этот процесс происходит из-за вызова функции, он называется неявным созданием экземпляра. Созданная функция часто называется экземпляром функции (для краткости – экземпляром) или шаблонной функцией. Экземпляры функций – это обычные во всех отношениях функции.

Процесс создания экземпляра функции прост: компилятор, по сути, клонирует шаблон функции и заменяет шаблонный тип (T) фактическим типом, который мы указали (int).

Поэтому, когда мы вызываем max<int>(1, 2), создаваемая функция выглядит примерно так:

template<>                 // пока игнорируем
int max<int>(int x, int y) // сгенерированная функция max<int>(int, int)
{
    return (x > y) ? x : y;
}

Вот тот же пример, что и выше, показывающий, что на самом деле компилирует компилятор после создания всех экземпляров:

#include <iostream>
 
template <typename T> 
T max(T x, T y); // объявление для нашего шаблона функции (определение нам больше не нужно)
 
template<>
int max<int>(int x, int y) // сгенерированная функция max<int>(int, int)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max<int>(1, 2) << '\n'; // создает и вызывает функцию max<int>(int, int)
 
    return 0;
}

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

Приведем еще один пример:

#include <iostream>
 
template <typename T>
T max(T x, T y) // шаблон функции для max(T, T)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max<int>(1, 2) << '\n';    // создает и вызывает функцию max<int>(int, int)
    std::cout << max<int>(4, 3) << '\n';    // вызывает уже созданную функцию max<int>(int, int)
    std::cout << max<double>(1, 2) << '\n'; // создает и вызывает функцию max<double>(double, double)
 
    return 0;
}

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

#include <iostream>
 
template <typename T> // объявление для нашего шаблона функции (определение нам больше не нужно)
T max(T x, T y);
 
template<>
int max<int>(int x, int y) // сгенерированная функция max<int>(int, int)
{
    return (x > y) ? x : y;
}
 
template<>
double max<double>(double x, double y) // сгенерированная функция max<double>(double, double)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max<int>(1, 2) << '\n';    // создает и вызывает функцию max<int>(int, int)
    std::cout << max<int>(4, 3) << '\n';    // вызывает уже созданную функцию max<int>(int, int)
    std::cout << max<double>(1, 2) << '\n'; // создает и вызывает функцию  max<double>(double, double)
 
    return 0;
}

Здесь следует отметить еще одну вещь: когда мы создаем экземпляр max<double>, созданная функция имеет параметры типа double. Но поскольку мы предоставили аргументы типа int, эти аргументы будут неявно преобразованы в double.

Вывод аргументов шаблона

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

std::cout << max<int>(1, 2) << '\n'; // указываем, что мы хотим вызвать max<int>

В этом вызове функции мы указали, что хотим заменить T на int, но мы также вызываем функцию с аргументами int.

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

Например, вместо такого вызова функции:

std::cout << max<int>(1, 2) << '\n'; // указываем, что мы хотим вызвать max<int>

Вместо этого мы можем сделать одно из следующих действий:

std::cout << max<>(1, 2) << '\n';
std::cout << max(1, 2) << '\n';

В любом случае компилятор увидит, что мы не предоставили фактический тип, поэтому он попытается вывести фактический тип из аргументов функции, что позволит ему сгенерировать функцию max(), где все параметры шаблона соответствуют типу предоставленных аргументов. В этом примере компилятор сделает вывод, что использование шаблона функции max<T> с фактическим типом int позволяет ему создать экземпляр функции max<int>(int, int), где тип обоих параметров шаблона (int) соответствует типу предоставленных аргументов (int).

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

Например:

#include <iostream>
 
template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}
 
int max(int x, int y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max<int>(1, 2) << '\n';// выбирает max<int>
    std::cout << max<>(1, 2) << '\n';   // выводит max<int>(int, int) (нешаблонные функции не рассматриваются)
    std::cout << max(1, 2) << '\n';     // вызывает функцию max(int, int)
 
    return 0;
}

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

Лучшая практика


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

Шаблоны функций с параметрами, не относящимися к шаблону

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

Например:

template <typename T>
int someFcn (T x, double y)
{
    return 5;
}
 
int main()
{
    someFcn(1, 3.4); // соответствует someFcn(int, double)
    someFcn(1, 3.4f); // соответствует someFcn(int, double) - float расширяется до double
    someFcn(1.2, 3.4); // соответствует someFcn(double, double)
    someFcn(1.2f, 3.4); // соответствует someFcn(float, double)
    someFcn(1.2f, 3.4f); // соответствует someFcn(float, double) - float расширяется до double
 
    return 0;
}

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

Созданные функции не всегда могут компилироваться

Рассмотрим следующую программу:

#include <iostream>
 
template <typename T>
T addOne(T x)
{
    return x + 1;
}
 
int main()
{
    std::cout << addOne(1) << '\n';
    std::cout << addOne(2.3) << '\n';
 
    return 0;
}

Компилятор эффективно скомпилирует и выполнит следующее:

#include <iostream>
 
template <typename T>
T addOne(T x);
 
template<>
int addOne<int>(int x)
{
    return x + 1;
}
 
template<>
double addOne<double>(double x)
{
    return x + 1;
}
 
int main()
{
    std::cout << addOne(1) << '\n';   // вызывает addOne<int>(int)
    std::cout << addOne(2.3) << '\n'; // вызывает addOne<double>(double)
 
    return 0;
}

что даст результат:

2
3.3

Но что, если мы попробуем что-то подобное?

#include <iostream>
#include <string>
 
template <typename T>
T addOne(T x)
{
    return x + 1;
}
 
int main()
{
    std::string hello { "Hello, world!" };
    std::cout << addOne(hello) << '\n';
 
    return 0;
}

Когда компилятор пытается разрешить addOne(hello), он не найдет не-шаблонной функции для addOne(std::string), но найдет наш шаблон функции для addOne(T) и определит, что он может сгенерировать из него addOne(std::string). Таким образом, компилятор сгенерирует и скомпилирует это:

#include <iostream>
#include <string>
 
template <typename T>
T addOne(T x);
 
template<>
std::string addOne<std::string>(std::string x)
{
    return x + 1;
}
 
int main()
{
    std::string hello{ "Hello, world!" };
    std::cout << addOne(hello) << '\n';
 
    return 0;
}

Однако это приведет к ошибке компиляции, потому что x + 1 не имеет смысла, когда x является std::string. Очевидное решение здесь – просто не вызывать addOne() с аргументом типа std::tring.

Обобщенное программирование

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

Заключение

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

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

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

Лучшая практика

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

Теги

C++ / CppLearnCppДля начинающихОбучениеПрограммированиеШаблон / TemplateШаблон функции

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.