Напишите один раз — используйте для любого типа
На предыдущей странице вы отсортировали vector<int> с помощью std::sort. Но std::sort сортирует и vector<string>, и vector<double>, и массив ваших собственных структур — при этом никто не пишет отдельный sort для каждого. Это не магия и не перегрузка. Это шаблон: единственный фрагмент кода, который компилятор переиспользует для любого типа, который вы ему передаёте.
Без шаблонов вы были бы вынуждены копировать одну и ту же логику для каждого типа. Вот одна и та же функция maximum, написанная трижды, — ровно то дублирование, ради устранения которого и существуют шаблоны:
int maximum(int a, int b) { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }
Тела идентичны. Различаются только типы. Шаблон позволяет сказать «это работает для любого типа T» и написать это один раз.
Шаблоны функций
Вы превращаете функцию в шаблон, добавляя перед ней template <typename T> и используя T везде, где обычно стоял бы конкретный тип.
Обратите внимание: вы нигде не писали maximum<int> или maximum<double>. Компилятор смотрит на аргументы и определяет, чем должно быть T, — это вывод аргументов шаблона. Каждый отдельный тип, с которым вы её вызываете, заставляет компилятор инстанцировать (сгенерировать) отдельную конкретную функцию за кулисами.
Вы можете указать тип явно, когда вывод не справляется, используя угловые скобки:
В выводе типов кроется распространённая ловушка. Поскольку T должно быть единственным типом, смешивание типов аргументов ломает его:
maximum(3, 7.5); // ОШИБКА: T - это int или double? Компилятор отказывается гадать.
Это можно исправить, указав тип явно — maximum<double>(3, 7.5) — или дав каждому параметру свой собственный параметр типа, что мы и сделаем дальше.
Несколько параметров типа
Шаблон не ограничен одним типом. Перечислите столько, сколько нужно, через запятую. Вот как написать функцию, параметры которой могут быть разных типов:
Когда тип возвращаемого значения зависит от параметров, позвольте компилятору вычислить его с помощью auto (C++14 и новее), который естественно сочетается с шаблонами:
Шаблоны классов
Шаблоны нужны не только для функций — шаблонами могут быть целые классы. Именно так работают стандартные контейнеры: vector<int>, словарь «ключ-значение» map<string, int> и pair<A, B> — все они шаблоны классов. Вы пишете структуру данных один раз, и она хранит тот тип, которым вы её параметризуете.
Вот крошечный обобщённый Box, хранящий одно значение любого типа:
Ключевое отличие от шаблонов функций: с шаблоном класса вам обычно приходится указывать тип в угловых скобках — Box<int> — потому что в старых стандартах нет аргументов конструктора, из которых его можно было бы вывести. (В C++17 добавили вывод аргументов шаблона класса, так что Box b(42); тоже работает, но указывать явно всегда безопасно и читается ясно.)
Ошибки будут огромными — вот почему
Это та часть, на которой все спотыкаются, поэтому скажем прямо. Шаблон полностью проверяется только тогда, когда он инстанцируется с реальным типом. Можно написать шаблон, который использует <, и сам по себе он прекрасно компилируется — ошибка появляется лишь в тот момент, когда вы инстанцируете его с типом, у которого нет <.
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b; // требует, чтобы T поддерживал >
}
struct Point { int x, y; };
// maximum(Point{1,2}, Point{3,4});
// ОШИБКА: нет operator > для Point. Сообщение называет Point И
// цитирует всю эту функцию, часто растягиваясь на многие строки.
Поскольку компилятор подставляет полный тип в шаблон и сообщает об ошибках изнутри сгенерированного кода, одна-единственная ошибка может породить стену вывода, упоминающую внутренности библиотеки. Два совета на выживание:
- Читайте первую ошибку, а не последнюю. Последующие ошибки обычно — следствие первой.
- Ищите в сообщении имя вашего собственного типа (здесь
Point). Оно подскажет, какое инстанцирование пошло не так.
Настоящее решение — убедиться, что ваш тип поддерживает всё, что нужно шаблону: для maximum это значит дать Point перегрузку оператора operator>, что относится к более поздней странице. Концепты современного C++20 могут вытащить эти ошибки раньше и сделать их читаемыми, но лежащая в основе модель подстановки остаётся той же.
Далее: Классы
Вы только что построили шаблон класса Box — класс с приватными данными, конструктором и методами — сосредоточившись на самих шаблонах. Следующая страница замедляется и учит классам как следует: как объединять данные с функциями, которые с ними работают, что на самом деле контролируют public и private и как методы обращаются к собственному состоянию объекта. Шаблоны и классы в реальном C++ постоянно сочетаются, так что прочное владение классами делает написание обобщённого кода куда проще.
Часто задаваемые вопросы
Что такое шаблон в C++?
Шаблон — это заготовка, которая позволяет написать функцию или класс один раз и заставить компилятор сгенерировать версию для каждого типа, с которым вы её используете. Вы пишете template <typename T>, а затем используете T вместо настоящего типа. Компилятор создаёт конкретную версию — это называется инстанцированием.
В чём разница между typename и class в шаблоне C++?
typename и class в шаблоне C++?В списке параметров шаблона template <typename T> и template <class T> означают ровно одно и то же. Сегодня обычно предпочитают typename, потому что он читается честнее: T может быть любым типом, а не только классом. Выбор ключевого слова никак не влияет на сгенерированный код.
Почему сообщения об ошибках шаблонов в C++ такие длинные?
Шаблоны проверяются, когда они инстанцируются с реальным типом, а не когда они пишутся. Если тип не поддерживает использованную вами операцию (например, < для сортировки), ошибка появляется в глубине кода библиотеки с полностью расписанным инстанцированным типом, выдавая страницы вывода. Читайте первую ошибку и ищите в ней имя вашего типа.