8.13 – Шаблоны функций
Допустим, вы хотели написать функцию для определения максимального из двух чисел. Вы можете сделать это так:
int max(int x, int y)
{
return (x > y) ? x : y;
}
Хотя вызывающий может передавать в функцию разные значения, тип параметров фиксирован, поэтому вызывающий может передавать только значения типа int
. Это означает, что эта функция хорошо работает на самом деле только для чисел int
(и типов, для которых может быть выполнено расширяющее преобразование до int
).
Итак, что будет позже, когда вы захотите найти максимум из двух значений double
? Поскольку C++ требует от нас указания типов всех параметров функции, решение состоит в создании новой перегруженной версии max()
с параметрами типа double
:
double max(double x, double y)
{
return (x > y) ? x: y;
}
Обратите внимание, что код для реализации версии max()
для double
точно такой же, как и для версии max()
для int
! Фактически, эта реализация работает для многих разных типов: включая int
, double
, long
, long double
и даже новых типов, которые вы создали сами (как это сделать, мы рассмотрим в будущих уроках).
Необходимость создавать перегруженные функции с одинаковой реализацией для каждого набора типов параметров, которые мы хотим поддерживать, будет головной болью при обслуживании, рецептом ошибок и явным нарушением принципа DRY (don’t repeat yourself, не повторяйся). Здесь также есть менее очевидная проблема: программист, который хочет использовать функцию max()
, может пожелать вызвать ее с типом параметра, который автор max()
не ожидал (и, следовательно, не написал для него перегруженную функцию).
Чего нам действительно не хватает, так это способа написать одну единственную версию max()
, которая может работать с аргументами любого типа (даже с типами, о которых, возможно, не задумывались, когда писался код для max()
). Обычные функции здесь просто не подходят. К счастью, C++ поддерживает еще одно средство, которое было разработано специально для решения такого рода задач.
Добро пожаловать в мир шаблонов C++.
Введение в шаблоны C++
В C++ система шаблонов была разработана для упрощения процесса создания функций (или классов), которые могут работать с разными типами данных.
Вместо того чтобы вручную создавать группу почти идентичных функций или классов (по одному для каждого набора разных типов), мы вместо этого создаем единый шаблон. Как и обычное определение, шаблон описывает, как выглядит функция или класс. В отличие от обычного определения (где должны быть указаны все типы), в шаблоне мы можем использовать один или несколько типов-заполнителей. Тип-заполнитель представляет собой какой-либо тип, который неизвестен на момент написания шаблона, но будет предоставлен позже.
После того, как шаблон определен, компилятор может использовать его для создания необходимого количества перегруженных функций (или классов), каждая из которых использует разные реальные типы!
Конечный результат тот же – мы получаем кучу в основном идентичных функций или классов (по одному для каждого набора разных типов). Но нам нужно создать и поддерживать только один шаблон, а всю тяжелую работу за нас делает компилятор.
Ключевые выводы
Компилятор может использовать один шаблон для создания семейства связанных функций или классов, каждый из которых использует свой набор типов.
В качестве отступления...
Поскольку концепцию шаблонов сложно описать словами, давайте попробуем провести аналогию.
Если бы вы искали слово «шаблон» в словаре, вы бы нашли определение, подобное следующему: «шаблон – это модель, которая служит образцом для создания похожих объектов». Например, один из типов шаблонов, который очень легко понять, – это трафарет. Трафарет – это тонкий кусок материала (например, кусок картона или пластика) с вырезанной в нем фигурой (например, лицо). Поместив трафарет поверх другого объекта, а затем, распылив краску через отверстие, вы можете очень быстро воспроизвести вырезанную фигуру. Сам трафарет нужно создать только один раз, а затем его можно использовать повторно сколько угодно раз, чтобы создавать вырезанную фигуру в сколь угодно разных цветах. Более того, цвет фигуры, созданной с помощью трафарета, не нужно определять до тех пор, пока трафарет не будет использован.
Шаблон – это, по сути, трафарет для создания функций или классов. Мы создаем шаблон (наш трафарет) один раз, а затем можем использовать его столько раз, сколько необходимо, для создания функции или класса для определенного набора фактических типов. Эти фактические типы не нужно определять до фактического использования шаблона.
Поскольку фактические типы не определяются до тех пор, пока шаблон не будет использован в программе (а не при написании шаблона), автору шаблона не нужно пытаться предвидеть все фактические типы, которые могут быть использованы. Это означает, что код шаблона можно использовать с типами, которых даже не существовало на момент его написания! Мы увидим, как это пригодится позже, когда мы начнем изучать стандартную библиотеку C++, которая полностью заполнена кодом шаблонов!
Ключевые выводы
Шаблоны могут работать с типами, которых даже не существовало на момент их написания. Это помогает сделать код шаблона гибким и ориентированным на будущее!
В оставшейся части этого урока мы познакомимся и рассмотрим, как создавать шаблоны для функций, а также более подробно опишем, как они работают. Мы отложим обсуждение шаблонов классов до тех пор, пока не разберемся, что такое классы.
Шаблоны функций
Шаблон функции – это определение, подобное функции, которое используется для создания одной или нескольких перегруженных функций, каждая из которых имеет свой набор фактических типов. Это то, что позволит нам создавать функции, которые могут работать с множеством разных типов.
Когда мы создаем наш шаблон функции, для любых типов параметров, возвращаемых типов или типов, используемых в теле функции, которые мы хотим указать позже, мы используем типы-заполнители (также называемые шаблонными типами).
Шаблоны функций лучше всего изучать на примере, поэтому давайте преобразуем нашу обычную функцию max(int, int)
из примера выше в шаблон функции. Это на удивление просто, и мы объясним, что происходит в процессе.
Создание шаблонной функции max
Вот снова версия max()
для int
:
int max(int x, int y)
{
return (x > y) ? x : y;
}
Обратите внимание, что мы используем тип int
в этой функции три раза: один раз для параметра x
, один раз для параметра y
и один раз для типа возвращаемого значения функции.
Чтобы создать шаблон функции, нам нужно сделать две вещи. Во-первых, мы собираемся заменить наши конкретные типы шаблонными типами. В этом случае, поскольку у нас есть только один тип, который нужно заменить (int
), нам нужен только один шаблонный тип. Обычно для обозначения шаблонных типов используются одиночные заглавные буквы (начиная с T
).
Вот наша новая функция, которая использует один шаблонный тип:
T max(T x, T y) // не будет компилироваться, потому что мы не определили T
{
return (x > y) ? x : y;
}
Лучшая практика
Для обозначения шаблонных типов используйте одиночные заглавные буквы (начиная с T
). Например, T
, U
, V
и т.д.
Это хорошее начало, но этот код не скомпилируется, потому что компилятор не знает, что такое T
! И это по-прежнему обычная функция, а не шаблон функции.
Во-вторых, мы собираемся сообщить компилятору, что это шаблон функции, а T
– это шаблонный тип. Это делается с помощью так называемого объявления параметра шаблона:
template <typename T> // это объявление параметра шаблона
T max(T x, T y) // это определение шаблона функции для max<T>
{
return (x > y) ? x : y;
}
Давайте подробнее рассмотрим объявление параметра шаблона. Начнем с ключевого слова template
, которое сообщает компилятору, что мы создаем шаблон. Затем, в угловых скобках (<>
) мы указываем все шаблонные типы, которые наш шаблон будет использовать. Для каждого шаблонного типа мы используем ключевое слово typename
или class
, за которым следует имя шаблонного типа (например, T
).
В качестве отступления...
В этом контексте нет разницы между ключевыми словами typename
и class
. Вы часто будете видеть, как люди используют ключевое слово class
, поскольку оно было введено в язык раньше. Однако мы предпочитаем новое ключевое слово typename
, потому что оно проясняет, что шаблонный тип может быть заменен любым типом (например, базовым типом), а не только типами классов.
Поскольку этот шаблон функции имеет один шаблонный тип (с именем T
), мы будем называть его max<T>
.
Связанный контент
Мы обсудим, как создавать шаблоны функций с несколькими шаблонными типами в уроке «8.15 – Шаблоны функций с несколькими шаблонными типами».
Вы не поверите, но мы закончили!
В следующем уроке мы рассмотрим, как мы используем наш шаблон функции max<T>
для создания функций max()
с параметрами разных типов.