Дисклаймер: статья была начата еще в феврале, но, по зависящим от меня причинам, закончена не была. Тема очень обширна, поэтому публикуется в урезанном виде. Что не поместилось, будет рассмотрено позже.
Невозможно разбираться в современном С++, не зная, что такое шаблоны программирования. Данное свойство языка открывает широкие возможности оптимизации и повторного использования кода. В данной статье попробуем разобраться, что это такое и как это всё работает.
Механизм шаблонов в языке С++ позволяет решать проблему унификации алгоритма для различных типов: нет необходимости писать различные функции для целочисленных, действительных или пользовательских типов – достаточно составить обобщенный алгоритм, не зависящий от типа данных, основывающийся только на общих свойствах. Например, алгоритм сортировки может работать как с целыми числами, так и с объектами типа «автомобиль».
Существуют шаблоны функций и шаблоны классов.
Шаблоны функций -– это обобщенное описание поведения функций, которые могут вызываться для объектов разных типов. Другими словами, шаблон функции (шаблонная функция, обобщённая функция) представляет собой семейство разных функций (или описание алгоритма). По описанию шаблон функции похож на обычную функцию: разница в том, что некоторые элементы не определены (типы, константы) и являются параметризованными.
Шаблоны классов -– обобщенное описание пользовательского типа, в котором могут быть параметризованы атрибуты и операции типа. Представляют собой конструкции, по которым могут быть сгенерированы действительные классы путём подстановки вместо параметров конкретных аргументов.
Рассмотрим более подробно шаблоны функций.
Шаблоны функций
Как написать первую шаблонную функцию?
Рассмотрим случай определения минимального элемента из двух. В случае целых и действительных чисел придется написать 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<class Type> (или template<typename Type>), что означает, что в алгоритме используется какой-то абстрактный тип Type;
- в реализации функции имя типа заменяется на Type.
Для функции min получится следующее:
template<class Type>
Type _min(Type a, Type b){
if( a < b){
return a;
}
return b;
}
Самым интересным является тот факт, что пока нет вызова функции min, при компиляции она в бинарном коде не создается (не инстанцируется). А если объявить группу вызовов функции с переменными различных типов, то для каждого компилятор создаст свою реализацию на основе шаблона.
Вызов шаблонной функции, в общем, эквивалентен вызову обыкновенной функции. В этом случае компилятор определит, какой тип использовать вместо Type, на основании типа фактических параметров. Но если подставляемые параметры окажутся разных типов, то компилятор не сможет вывести (инстанцировать шаблон) реализацию шаблона. Так, в ниже следующем коде компилятор споткнётся на третьем вызове, так как не может определить, чему равен Type (подумайте, почему?):
#include <iostream>
template<class Type>
Type _min(Type a, Type b) {
if (a < b) {
return a;
}
return b;
}
int main(int argc, char** argv) {
std::cout << _min(1, 2) << std::endl;
std::cout << _min(3.1, 1.2) << std::endl;
std::cout << _min(5, 2.1) << std::endl; // oops!
return 0;
}
Решается эта проблема указанием конкретного типа при вызове функции.
#include <iostream>
template<class Type>
Type _min(Type a, Type b) {
if (a < b) {
return a;
}
return b;
}
int main(int argc, char** argv) {
std::cout << _min<double>(5, 2.1) << std::endl;
return 0;
}
Когда шаблонная функция (не) будет работать?
В принципе, можно понять, что компилятор просто подставляет нужный тип в шаблон. Но всегда ли получаемая функция будет работоспособна? Очевидно, что нет. Любой алгоритм может быть определен независимо от типа данных, но он обязательно пользуется свойствами этих данных. В случае с шаблонной функцией _min это требование определения оператора упорядочения (оператор <).
Любой шаблон функции предполагает наличие определенных свойств параметризованного типа, в зависимости от реализации (например, оператора копирования, оператора сравнения, наличия определенного метода и т.д.). В ожидаемом стандарте языка С++ за это будут отвечать концепции.
Перегрузка шаблона функции
Шаблоны функций также могут перегружаться. Обычно данная перегрузка выполняется при
template<class Type>
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;
}
Специализация шаблона для конкретных типов делается опять же из соображения экономичности: если эта версия шаблона функции в коде не используется, то она не будет включена в бинарный код.
На будущее остаются множественные и целочисленные параметры. Естественным продолжением являются шаблоны классов, основы порождающего программирования, и устройство стандартной библиотеки С++. И куча примеров!
Комментарии (9)
ncr
09.06.2019 22:35+2_min
Не делайте так сами и не учите других: the identifiers that begin with an underscore are reserved in the global namespace.
байткод
Вы точно про C++ пишете?mcroitor Автор
09.06.2019 22:39Когда я учился, таких правил в стандарте не было. Впрочем, считайте, что всё прописано в пространстве имён.
Насчет байткода описка, исправил, спасибо.Ryppka
10.06.2019 11:05Вы учились до 1989 года? Вроде в стандарте языка C, это уже было, C++ просто унаследовал правила.
MWGuy
10.06.2019 07:02-1Могу от себя добавить что шаблон это заметка для компилятора. Таким образом но будет компилировать развёрнутый шаблон когда это нужно,
iHateCpp
10.06.2019 08:10Я просто оставлю это здесь
CppCon 2018: Walter E. Brown “C++ Function Templates: How Do They Really Work?”
www.youtube.com/watch?v=NIDEjY5ywqU
Videoman
10.06.2019 19:04Как-то слишком поверхностно. Теперь у начинающего будет полная каша в голове. Он будет думать чем это все отличается от перегрузки функции и когда что будет вызываться если в коде есть и то, и то.
savostin
Самый ад — это отладка шаблонных функций. Особенно чужих. Особенно Boost.
kloppspb
Да. Буст в отладчике — это лютый трындец.