Что такое template, как и зачем?
Шаблоны (template) в C++ являются отдельными синтаксическими конструкциями.
Шаблоны используются для обобщённого программирования. Вы пишете алгоритм, который работает с различными типами данных, требуя от них только некоторой небольшой функциональности, а компилятор при использовании этого шаблона с конкретным типом подставляет тип и фактически сам генерирует требуемый код на C++.
Вот простейший пример использования шаблонов:
template T min(const T x, const T y)
Мы написали код, который работает с любым типом данных, требуя от этого типа только наличия операции «меньше» (operator<). (На самом деле ещё конструктора копирования, но это уже детали.) Теперь мы можем использовать наш шаблон:
int x, y; int z = min(x,y); // Автоматический вывод типа. double a, b; double c = min(a,b); // Автоматический вывод типа. double d = min(x,y); // Явное указание типа. std::vector::iterator i, j, k; k = min(i,j);
Видно, что нам удалось использовать этот шаблон не только к обычным числовым типам, но и к сложным сущностям — итераторам.
В этом мощь обобщённого программирования — код, написанный один раз используется многократно. Но есть и недостаток — «разбухание» бинарного кода — для каждого типа параметра шаблона компилятор создаст свой бинарный код.
Основы шаблонов С++: шаблоны функций
Дисклаймер: статья была начата еще в феврале, но, по зависящим от меня причинам, закончена не была. Тема очень обширна, поэтому публикуется в урезанном виде. Что не поместилось, будет рассмотрено позже.

Невозможно разбираться в современном С++, не зная, что такое шаблоны программирования. Данное свойство языка открывает широкие возможности оптимизации и повторного использования кода. В данной статье попробуем разобраться, что это такое и как это всё работает.
Механизм шаблонов в языке С++ позволяет решать проблему унификации алгоритма для различных типов: нет необходимости писать различные функции для целочисленных, действительных или пользовательских типов – достаточно составить обобщенный алгоритм, не зависящий от типа данных, основывающийся только на общих свойствах. Например, алгоритм сортировки может работать как с целыми числами, так и с объектами типа «автомобиль».
Существуют шаблоны функций и шаблоны классов.
Шаблоны функций -– это обобщенное описание поведения функций, которые могут вызываться для объектов разных типов. Другими словами, шаблон функции (шаблонная функция, обобщённая функция) представляет собой семейство разных функций (или описание алгоритма). По описанию шаблон функции похож на обычную функцию: разница в том, что некоторые элементы не определены (типы, константы) и являются параметризованными.
Шаблоны классов -– обобщенное описание пользовательского типа, в котором могут быть параметризованы атрибуты и операции типа. Представляют собой конструкции, по которым могут быть сгенерированы действительные классы путём подстановки вместо параметров конкретных аргументов.
Рассмотрим более подробно шаблоны функций.
Шаблоны функций
Как написать первую шаблонную функцию?
Рассмотрим случай определения минимального элемента из двух. В случае целых и действительных чисел придется написать 2 функции.
int _min(int a, int b) < if( a < b)< return a; >return b; > double _min(double a, double b) < if( a < b)< return a; >return b; >
Можно, конечно, реализовать только одну функцию, с действительными параметрами, но для понимания шаблонов это будет вредным.
Что произойдёт в случае компиляции приложения? Обе реализации функции попадут в бинарный код приложения, даже если они не используются (впрочем, сейчас компиляторы очень умные, умеют вырезать неиспользуемый код). А если необходимо добавить функцию, определяющую минимальную из 2 строк (сложно представить без уточнения, что есть минимальная строка)?!
В этом случае, если алгоритм является общим для типов, с которыми приходится работать, можно определить шаблон функции. Принцип, в общем случае, будет следующим:
- берётся реализация функции для какого-то типа;
- приписывается заголовок template (или template), что означает, что в алгоритме используется какой-то абстрактный тип Type;
- в реализации функции имя типа заменяется на Type.
template Type _min(Type a, Type b) < if( a < b)< return a; >return b; >
Самым интересным является тот факт, что пока нет вызова функции min, при компиляции она в бинарном коде не создается (не инстанцируется). А если объявить группу вызовов функции с переменными различных типов, то для каждого компилятор создаст свою реализацию на основе шаблона.
Вызов шаблонной функции, в общем, эквивалентен вызову обыкновенной функции. В этом случае компилятор определит, какой тип использовать вместо Type, на основании типа фактических параметров. Но если подставляемые параметры окажутся разных типов, то компилятор не сможет вывести (инстанцировать шаблон) реализацию шаблона. Так, в ниже следующем коде компилятор споткнётся на третьем вызове, так как не может определить, чему равен Type (подумайте, почему?):
#include template Type _min(Type a, Type b) < if (a < b) < return a; >return b; > int main(int argc, char** argv) < std::cout
Решается эта проблема указанием конкретного типа при вызове функции.
#include template Type _min(Type a, Type b) < if (a < b) < return a; >return b; > int main(int argc, char** argv) < std::cout (5, 2.1)
Когда шаблонная функция (не) будет работать?
В принципе, можно понять, что компилятор просто подставляет нужный тип в шаблон. Но всегда ли получаемая функция будет работоспособна? Очевидно, что нет. Любой алгоритм может быть определен независимо от типа данных, но он обязательно пользуется свойствами этих данных. В случае с шаблонной функцией _min это требование определения оператора упорядочения (оператор <).
Любой шаблон функции предполагает наличие определенных свойств параметризованного типа, в зависимости от реализации (например, оператора копирования, оператора сравнения, наличия определенного метода и т.д.). В ожидаемом стандарте языка С++ за это будут отвечать концепции.
Перегрузка шаблона функции
Шаблоны функций также могут перегружаться. Обычно данная перегрузка выполняется при
template Type* _min(Type* a, Type* b) < if(*a < *b)< return a; >return b; >
Частные случаи
В некоторых случаях шаблон функции является неэффективным или неправильным для определенного типа. В этом случае можно специализировать шаблон, — то есть написать реализацию для данного типа. Например, в случае со строками можно потребовать, чтобы функция сравнивала только количество символов. В случае специализации шаблона функции тип, для которого уточняется шаблон в параметре не указывается. Ниже приводится пример указанной специализации.
template<> std::string _min(std::string a, std::string b) < if(a.size() < b.size())< return a; >return b; >
Специализация шаблона для конкретных типов делается опять же из соображения экономичности: если эта версия шаблона функции в коде не используется, то она не будет включена в бинарный код.
На будущее остаются множественные и целочисленные параметры. Естественным продолжением являются шаблоны классов, основы порождающего программирования, и устройство стандартной библиотеки С++. И куча примеров!
Шаблоны
Шаблоны классов позволяют определить конструкции (функции, классы), которые используют определенные типы, но на момент написания кода точно не известно, что это будут за типы. Иными словами, шаблоны позволяют определить универсальные конструкции, которые не зависят от определенного типа.
Шаблоны функций (function template) позволяют определять функции, которые не зависят от конкретных типов.
Вначале рассмотрим пример, где это может пригодиться. Например, нам надо определить функцию для сложения двух чисел int , double и std::string . Первое, что приходит на ум, сделать перегрузку функции - для каждого типа определить свою версию:
#include int add(int, int); double add(double, double); std::string add(std::string, std::string); int main() < std::cout int add(int x, int y) < return x + y; >double add(double x, double y) < return x + y; >std::string add(std::string str1, std::string str2)
Данный пример отлично работает, производит вычисления, как и должен. Однако в данном случае мы сталкиваемся с тем, что функция add фактически повторяется. Ее версии фактически выполняют одно и то же действие, единственно что отличается тип параметров и возвращаемого значения.
Теперь применим шаблоны функций. Шаблоны функций представляют некоторый образец, по которому можно создать конкретную функцию, специфическую для определенного типа:
#include template T add(T, T); // прототип функции int main() < std::cout template T add(T a, T b)
Определение шаблона функции начинается с ключевого слова template , после которого в угловых скобках идет слово typename и затем список параметров шаблона:
template T add(T a, T b)
В данном случае после typename указан один параметр - T . Параметр шаблона представляет произвольный идентификатор, в качестве которого, как правило, применяются заглавные буквы, например, T. Но это необязательно. То есть в данном случае параметр T будет представлять некоторый тип, который становится известным во время компиляции. Это может быть и тип int, и double, и string, и любой другой тип. Но поскольку внутри функции мы применяем операцию сложения, важно, чтобы тип, который будет применяться вместо параметра T, поддерживал операцию сложения, которая возвращала бы объект этого же типа. Если вдруг используемый тип не будет применять операцию сложения, то на этапе компиляции мы столкнемся с ошибкой.
И при вызове функции add в нее можно передавать объекты и типа int, и типа double, и любого другого типа. При вызове функции компилятор на основании типа аргументов выведет конкретный тип, связанный с параметром шаблона T, и создаст экземпляр функции add, который работает с конкретным типом, и при вызове функции будет вызваться данный экземпляр функции. Если для последующего вызова функции требуется тот же экземпляр, то компилятор использует существующий экземпляр функции.
При этом также можно использовать ссылки, указатели, массивы, которые представляют тип параметра шаблона. Например, в программе выше передадим параметры по ссылке и сделаем их константными:
#include template T add(const T&, const T&); int main() < std::cout template T add(const T& a, const T& b)
Другой пример - функция обмена значениями:
#include template void swap(T&, T&); int main() < int c ; int d ; swap(c, d); std::cout #include template T* max(T*, T*); int main() < int a, b; std::cout , d; std::cout template T* max(T* a, T* b) < return *a >*b? a : b; >
Явный вызов реализации функции для определенного типа
В примерах выше компилятор на основании параметров определял, какой именно тип использует функция. Однако мы можем также явным образом указать, какой тип мы хотим использовать:
#include template T add(const T&, const T&); int main() < double d < add(3.3, 2.2)>; std::cout << "d: " << d << std::endl; //d: 5.5 d = add(3, 2); std::cout template T add(const T& a, const T& b)
Здесь мы явным образом указываем, что мы хотим использовать тип double :
double d < add(3.3, 2.2)>;
При этом во втором случае в функцию передаются целочисленные литералы, однако мы все равно используем реализацию функции для типа double :
d = add(3, 2);
Перегрузка функций и параметризация
С одной стороны, параметризация позволяет снизить возможность перегрузки функций, так как мы можем абстрагироваться от конкретных типов. С другой стороны, все равно могут быть ситуации, когда необходимы разные версии функции. И тут мы можем совместить перегрузку функций и их параметризацию. Например, нам нужно найти максимальное значение из двух, либо из набора элементов:
#include template const T* max(const T*, const T*); template const T* max(const T[], unsigned); int main() < int a, b; std::cout ; std::cout template const T* max(const T* a, const T* b) < return *a >*b? a : b; > template const T* max(const T data[], unsigned size) < const T* result <>; // если вектор пуст, то возвращается nullptr for(unsigned i<>; i < size; i++) < // если result не равен nullptr и *result меньше value if (!result || data[i] >*result) result = &data[i]; > return result; >
Здесь определены две параметризированные функции для нахождения максимального значения. Но одна из них просто сравнивает два значения по адресам в указателях, а вторая проходит по всем элементам массива и выбирает из них наибольший.
Использование нескольких параметров
Можно использовать несколько параметров. Например, нам надо определить функцию перевода некоторой суммы с одного счета на другой и передать код операции. Для идентификаторов счета и кода операции мы можем использовать разные типы - числа, строки и т.д. Для этого определим следующую функцию:
#include template void transfer(T, T, K, int); int main() < transfer("id1234", "id5678", 2804, 5000); >template void transfer(T fromAccount, T toAccount, K code, int sum)
В данном случае при вызове transfer("id1234", "id5678", 2804, 5000); вместо параметра T будет подставляться символьный массив, а вместо параметра K - тип int.
Выведение типов и decltype(auto)
Иногда может быть неизвестно, значение какого именного типа будет возвращаться, или мы захотим предоставить компилятору выводить точный тип возвращаемых данных. В этом случае вместо возвращаемого типа можно использовать заменитель decltype(auto) . Например, определим функцию вычисления среднего арифметического с выведением результата:
#include template decltype(auto) average(const T (&data)[], unsigned size) < T result <>; for(unsigned i<>; i < size; i++) < result += data[i]; >return result / size; > int main() < int numbers[]; std::cout
Нетипизированные параметры
С++ позволяет определять шаблоны с нетипизированными параметрами, то есть как и в функции, мы можем определять обычные параметры конкретных типов, например:
#include template void print(const T&); int main() < print(3); > // печатаем value N раз template void print(const T& value) < for(unsigned i<>; i < N; i++) < std::cout >
Здесь определен шаблон функции print, которая печатает значение value, которое представляет тип T, N раз. При этом параметр N имеет четко установленный тип - unsigned int . При вызове функции параметру N можно передать значение. Например, напечатаем 4 раза число 3:
print(3);
Большого смысла такие параметры не имеют, поскольку мы равным образом можем определить стандартные параметры в функции. Однако в ряде сценариев они могут оказаться полезными. Например, вычислим длину любого массива:
#include template size_t size(const T (&data)[N]) < return N; >int main() < const int numbers1[]; const double numbers2[]; const char* people[] ; std::cout
Другой пример - в одном из листингов выше для вычисления среднего значения чисел массива в функцию передавался размер массива. Но с помощью нетипизированных параметров мы можем избавиться от необходимости передавать в функцию размер массива:
#include template T average(const T (&)[N]); int main() < int numbers1[]; std::cout ; std::cout template T average(const T (&data)[N]) < T result <>; for(unsigned i<>; i < N; i++) < result += data[i]; >return result / N; >
Здесь для представления размера массива определен параметр N типа size_t . При вызове функции на основе переданного в функцию массива этот параметр получает конкретное значение - размер массива.
Выведение типов и результата
Стоит отметить, что начиная со стандарта C++20 можно определять параметры, типы которых автоматически выводятся исходя из переданных аргументов. Аналогично можно и выводить тип результата. Для этого применяется ключевое слово auto . Равным образом для определения типов параметров и результатов функции можно использовать выражения auto* , auto& и const auto& :
#include > auto add(const auto& a, const auto& b) < return a + b; >int main() < const int n1; const int n2; std::cout ; const double d2; std::cout
Template c что это
Шаблон класса (class template) позволяет задать внутри класса объекты, тип которых на этапе написания кода неизвестен. Но прежде чем перейти к определению шаблона класса, рассмотрим проблему, с которой мы можем столкнуться и которую позволяют решить шаблоны.
Допустим, нам надо описать класс пользователя, которые хранит два имя и id (идентификатор), который отличает одного пользователя от другого. С именем все относителньо просто - это строка. А какой тип данных выбрать для хранения id? Мы можем хранить id как число, как строку, как данные какого-то другого типа данных. И каждый тип в разных ситуациях может иметь свои преимущества. Как правило, для id применяются числа и строки, и, на первый взгляд, мы можем просто определить два класса для разных типов:
#include // класс Person, где id - целое число class UintPerson < public: UintPerson(unsigned id, std::string name) : id, name < >void print() const < std::cout private: unsigned id; std::string name; >; // класс Person, где id - строка class StringPerson < public: StringPerson(std::string id, std::string name) : id, name < >void print() const < std::cout private: std::string id; std::string name; >; int main() < UintPerson tom; tom.print(); // Id: 123456 Name: Tom StringPerson bob; bob.print(); // Id: tvi4xhcfhr Name: Bob >
Здесь класс UintPerson представляет класс пользователя, где id представляет целое число типа unsinged, а тип StringPerson - класс пользователя, где id - строка. В функции main мы можем создавать объекты этих типов и успешно их использовать. Хотя данный пример работает, но по сути мы получаем два идентичных класса, которые отличаются только типом переменной id. А что, если для id потребуется использовать какой-то еще тип? Чтобы упростить код в C++ можно использовать шаблоны классов.
Шаблоны классов позволяют уменьшить повторяемость кода. Для определения шаблона класса применяется следующий синтаксис:
template class имя класса < // содержимое шаблона класса >;
Для применения шаблонов перед классом указывается ключевое слово template , после которого идут угловые скобки. В угловых скобках указываются параметры шаблона. Если несколько параметров шаблона, то они указываются через запятую.
Сам шаблон класса, как и обычный класс, всегда начинается с ключевого слова class (или struct , если речь о структуре), за которым следует имя шаблона класса и тело определения в фигурных скобках. Как и в случае с обычным классом, все шаблон класса заканчивается точкой с запятой. Содержимое шаблона класса фактически аналогично определению стандартного класса за тем исключением, что внутри шаблона вместо конкретных типов мы можем использовать параметры шаблона, которые указаны в угловых скобках. Во всем остальном шаблон класса подобен обычному классу, который может наследоваться, определять функции, переменные, конструкторы, переопределять виртуальные функции и т.д.
Параметр в угловых скобках представляет произвольный идентификатор, перед которым указывается слово typename или class :
template // или так template
Здесь определен один параметр, который называется T . Какое слово перед ним использовать - class или typename , не столь важно.
Перепишем пример с классами UintPerson и StringPerson, применив шаблоны:
#include template class Person < public: Person(T id, std::string name) : id, name < >void print() const < std::cout private: T id; std::string name; >; int main() < Person tom; // T - число tom.print(); // Id: 123456 Name: Tom Person bob; // T - строка bob.print(); // Id: tvi4xhcfhr Name: Bob >
В данном случае шаблон класса применяет один параметр - T . То есть это будет какой-то тип, но какой именно, на этапе написания кода неизвестно.
template class PersonДанный параметр T будет представлять тип переменной id:
T id;При создании объектов шаблона класса Person, компилятор на основании первого параметра конструктора будет выводить тип id. Например, в первом случае:
Person tom;полю id передается число 123456. Поскольку это числовой литерал типа int, то и id будет представлять тип int .
Во втором случае
Person bob;переменной id передается строка "tvi4xhcfhr" - это литерал типа const char* , соответственно id будет представлять этот тип.
В этом случае компилятор будет создавать два определения класса - для каждого набора типов - для int и для const char* и будет использовать эти определения классов для создания его объектов, которые применяют определенный тип данных для id.
В примере выше тип id определялся автоматически. Но мы также можем явным образом указать тип в угловых скобках после названия класса:
int main() < Persontom; tom.print(); // Id: 123456 Name: Tom Person bob; bob.print(); // Id: tvi4xhcfhr Name: Bob >Также можно применять сразу несколько параметров. Например, необходимо определить класс банковского перевода:
#include template class Transaction < public: Transaction(T fromAcc, T toAcc, V code, unsigned sum): fromAccount, toAccount, code, sum < >void print() const < std::cout private: T fromAccount; // с какого счета T toAccount; // на какой счет V code; // код операции unsigned sum; // сумма перевода >; int main() < // явная типизация Transactiontransaction1; transaction1.print(); // From: id1234 To: id5678 Sum: 5000 Code: 2804 // неявная типизация Transaction transaction2; transaction2.print(); // From: id6789 To: id9018 Sum: 6000 Code: 3000 >Класс Transaction использует два параметра типа T и V. Параметр T определяет тип для счетов, которые участвуют в процессе перевода. Здесь в качестве номеров счетов можно использовать и числовые и строковые значения и значения других типов. А параметр V задает тип для кода операции - опять же это может быть любой тип.
При использовании шаблона в этом случае надо указать два типа:
Transaction transaction1("id1234", "id5678", 2804, 5000);Типы передаются параметрам по позиции. Так, тип string будет использоваться вместо параметра T, а тип int - вместо параметра V.
В случае с переменной transaction2 типы T и V выводятся исходя из параметров конструктора.
Определение функций вне шаблона класса
Синтаксис определения функций вне шаблона класса может немного отличаться от их определения внутри шаблона. В частности, определения функций вне шаблона класса должны определяться как шаблон, даже если они не используют параметры шаблона.
При определении конструктора вне шаблона класса, его имя должно уточняться именем шаблона класса:
#include template class Person < public: Person(T, std::string); // обычный конструктор Person(const Person&); // конструктор копирования ~Person(); // деструктор Person& operator=(const Person&); // оператор присваивания void print() const; // функция класса private: T id; std::string name; >; // определение конструктора вне шаблона класса template Person::Person(T id, std::string name) : id, name < >// определение конструктора копирования вне шаблона класса template Person::Person(const Person& person) : id, name < >// определение деструктора копирования вне шаблона класса template Person::~Person() < std::cout // определение оператора присвоения вне шаблона класса template Person& Person::operator=(const Person& person) < if (&person != this) < name = person.name; >return *this; > // определение функции вне шаблона класса template void Person::print() const < std::cout int main() < Person tom; tom.print(); Person tomas; // конструктор копирования tomas.print(); Person tommy = tom; // оператор присваивания tommy.print(); >В данном случае все функции, в том числе конструкторы, деструктор, функция оператора присваивания, определяются как функции шаблона класса Person . Причем в данном случае конструктор копирования или функция print никак не используют параметр T, но все равно они определяются как шаблоны. То же самое касается и деструктора.
Параметры шаблонов по умолчанию
Как и параметры функций, параметры шаблонов могут иметь значения по умолчанию - тип по умолчанию, который будет использоваться. Например:
#include template class Person < public: Person(std::string name) : name < >void setId(T value) < void print() const < std::cout private: T id; std::string name; >; int main() < Personbob; // T - std::string bob.setId("id1345"); bob.print(); // Id: id1345 Name: Bob Person tom; // T - int tom.setId(23456); tom.print(); // Id: 23456 Name: Tom >Здесь для параметра шаблона в качестве типа по умолчанию используется тип int . Параметр шаблона определяет тип переменной id, которую можно установить через функцию setId.
Мы можем указать тип в угловых скобках явным образом:
Person bob; // T - std::string bob.setId("id1345");В данном случае в качестве типа параметра шаблона применяется тип std::string , соответственно id будет представлять строку.
Во втором случае тип явным образом не указывается, поэтому применяется тип по умолчанию - int:
Person tom; // T - int tom.setId(23456);Поэтому здесь id будет представлять число.