Menu

Шаблоны C++: обобщённые функции и классы простыми словами

Напишите код один раз и заставьте его работать для любого типа с помощью шаблонов C++ — шаблоны функций, шаблоны классов, вывод типов и те запутанные ошибки компилятора, которые они порождают.

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

Напишите один раз — используйте для любого типа

На предыдущей странице вы отсортировали 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++?

В списке параметров шаблона template <typename T> и template <class T> означают ровно одно и то же. Сегодня обычно предпочитают typename, потому что он читается честнее: T может быть любым типом, а не только классом. Выбор ключевого слова никак не влияет на сгенерированный код.

Почему сообщения об ошибках шаблонов в C++ такие длинные?

Шаблоны проверяются, когда они инстанцируются с реальным типом, а не когда они пишутся. Если тип не поддерживает использованную вами операцию (например, < для сортировки), ошибка появляется в глубине кода библиотеки с полностью расписанным инстанцированным типом, выдавая страницы вывода. Читайте первую ошибку и ищите в ней имя вашего типа.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ