Основы шаблонов С++: шаблоны функций
Дисклаймер: статья была начата еще в феврале, но, по зависящим от меня причинам, закончена не была. Тема очень обширна, поэтому публикуется в урезаном виде. Что не поместилось, будет рассмотренно позже.
Невозможно разбираться в современном С++, не зная, что такое шаблоны программирования. Данное свойство языка открывает широкие возможности оптимизации и повторного использования кода. В данной статье попробуем разобраться, что это такое и как это всё работает.
Механизм шаблонов в языке С++ позволяет решать проблему унификации алгоритма для различных типов: нет необходимости писать различные функции для целочисленных, действительных или пользовательских типов — достаточно составить обобщенный алгоритм, не зависящий от типа данных, основывающийся только на общих свойствах. Например, алгоритм сортировки может работать как с целыми числами, так и с объектами типа «автомобиль».
Существуют шаблоны функций и шаблоны классов.
Шаблоны функций -– это обобщенное описание поведения функций, которые могут вызываться для объектов разных типов. Другими словами, шаблон функции (шаблонная функция, обобщённая функция) представляет собой семейство разных функций (или описание алгоритма). По описанию шаблон функции похож на обычную функцию: разница в том, что некоторые элементы не определены (типы, константы) и являются параметризованными.
Шаблоны классов -– обобщенное описание пользовательского типа, в котором могут быть параметризованы атрибуты и операции типа. Представляют собой конструкции, по которым могут сгенерированы действительные классы путём подстановки вместо параметров конкретных аргументов.
Рассмотрим более подробно шаблоны функций.
Шаблоны функций
Как написать первую шаблонную функцию?
Рассмотрим случай определения минимального элемента из двух. В случае целых и действительных чисел придется написать 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.
Для функции min получится следующее:
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 << _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
template
Type _min(Type a, Type b) {
if (a < b) {
return a;
}
return b;
}
int main(int argc, char** argv) {
std::cout << _min(5, 2.1) << std::endl;
return 0;
}
Когда шаблонная функция (не) будет работать?
В принципе, можно понять, что компилятор просто подставляет нужный тип в шаблон. Но всегда ли получаемая функция будет работоспособна? Очевидно, что нет. Любой алгоритм может быть определен независимо от типа данных, но он обязательно пользуется свойствами этих данных. В случае с шаблонной функцией _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;
}
Специализация шаблона для конкретных типов делается опять же из соображения экономичности: если эта версия шаблона функции в коде не используется, то она не будет включена в байткод.
На будущее остаются множественные и целочисленные параметры. Естественным продолжением являются шаблоны классов, основы порождающего программирования, и устройство стандартной библиотеки С++. И куча примеров!