Определение шаблона функции
Иногда может показаться, что сильно типизированный язык создает препятствия для реализации совсем простых функций. Например, хотя следующий алгоритм функции min() тривиален, сильная типизация требует, чтобы его разновидности были реализованы для всех типов, которые мы собираемся сравнивать:
int min( int a, int b ) {
return a < b ? a : b;
}
double min( double a, double b ) {
return a < b ? a : b;
}
Заманчивую альтернативу явному определению каждого экземпляра функции min() представляет использование макросов, расширяемых препроцессором:
#define min(a, b) ((a) < (b) ? (a) : (b))
Но этот подход таит в себе потенциальную опасность. Определенный выше макрос правильно работает при простых обращениях к min(), например:
min( 10, 20 );
min( 10.0, 20.0 );
но может преподнести сюрпризы в более сложных случаях: такой механизм ведет себя не как вызов функции, он лишь выполняет текстовую подстановку аргументов. В результате значения обоих аргументов оцениваются дважды: один раз при сравнении a и b, а второй – при вычислении возвращаемого макросом результата:
#include <iostream>
#define min(a,b) ((a) < (b) ? (a) : (b))
const int size = 10;
int ia[size];
int main() {
int elem_cnt = 0;
int *p = &ia[0];
// подсчитать число элементов массива
while ( min(p++,&ia[size]) != &ia[size] )
++elem_cnt;
cout << "elem_cnt : " << elem_cnt
<< "\texpecting: " << size << endl;
return 0;
}
На первый взгляд, эта программа подсчитывает количество элементов в массиве ia целых чисел. Но в этом случае макрос min() расширяется неверно, поскольку операция постинкремента применяется к аргументу-указателю дважды при каждой подстановке. В результате программа печатает строку, свидетельствующую о неправильных вычислениях:
elem_cnt : 5 expecting: 10
Шаблоны функций предоставляют в наше распоряжение механизм, с помощью которого можно сохранить семантику определений и вызовов функций (инкапсуляция фрагмента кода в одном месте программы и гарантированно однократное вычисление аргументов), не принося в жертву сильную типизацию языка C++, как в случае применения макросов.
Шаблон дает алгоритм, используемый для автоматической генерации экземпляров функций с различными типами. Программист параметризует все или только некоторые типы в интерфейсе функции (т.е. типы формальных параметров и возвращаемого значения), оставляя ее тело неизменным. Функция хорошо подходит на роль шаблона, если ее реализация остается инвариантной на некотором множестве экземпляров, различающихся типами данных, как, скажем, в случае min().
Так определяется шаблон функции min():
template <class Type>
Type min2( Type a, Type b ) {
return a < b ? a : b;
}
int main() {
// правильно: min( int, int );
min( 10, 20 );
// правильно: min( double, double );
min( 10.0, 20.0 );
return 0;
}
Если вместо макроса препроцессора min() подставить в текст предыдущей программы этот шаблон, то результат будет правильным:
elem_cnt : 10 expecting: 10
(В стандартной библиотеке C++ есть шаблоны функций для многих часто используемых алгоритмов, например для min(). Эти алгоритмы описываются в главе 12. А в данной вводной главе мы приводим собственные упрощенные версии некоторых алгоритмов из стандартной библиотеки.)
Как объявление, так и определение шаблона функции всегда должны начинаться с ключевого слова template, за которым следует список разделенных запятыми идентификаторов, заключенный в угловые скобки '<' и '>', – список параметров шаблона, обязательно непустой. У шаблона могут быть параметры-типы, представляющие некоторый тип, и параметры-константы, представляющие фиксированное константное выражение.
Параметр-тип состоит из ключевого слова class или ключевого слова typename, за которым следует идентификатор. Эти слова всегда обозначают, что последующее имя относится к встроенному или определенному пользователем типу. Имя параметра шаблона выбирает программист. В приведенном примере мы использовали имя Type, но могли выбрать и любое другое:
template <class Glorp>
Glorp min2( Glorp a, Glorp b ) {
return a < b ? a : b;
}
При конкретизации ( порождении конкретного экземпляра) шаблона вместо параметра-типа подставляется фактический встроенный или определенный пользователем тип. Любой из типов int, double, char*, vector<int> или list<double> является допустимым аргументом шаблона.
Параметр-константа выглядит как обычное объявление. Он говорит о том, что вместо имени параметра должно быть подставлено значение константы из определения шаблона. Например, size – это параметр-константа, который представляет размер массива arr:
template <class Type, int size>
Type min( Type (&arr) [size] );
Вслед за списком параметров шаблона идет объявление или определение функции. Если не обращать внимания на присутствие параметров в виде спецификаторов типа или констант, то определение шаблона функции выглядит точно так же, как и для обычных функций:
template <class Type, int size>
Type min( const Type (&r_array)[size] )
{
/* параметризованная функция для отыскания
* минимального значения в массиве */
Type min_val = r_array[0];
for ( int i = 1; i < size; ++i )
if ( r_array[i] < min_val )
min_val = r_array[i];
return min_val;
}
В этом примере Type определяет тип значения, возвращаемого функцией min(), тип параметра r_array и тип локальной переменной min_val; size задает размер массива r_array. В ходе работы программы при использовании функции min() вместо Type могут быть подставлены любые встроенные и определенные пользователем типы, а вместо size – те или иные константные выражения. (Напомним, что работать с функцией можно двояко: вызвать ее или взять ее адрес).
Процесс подстановки типов и значений вместо параметров называется конкретизацией шаблона. (Подробнее мы остановимся на этом в следующем разделе.)
Список параметров нашей функции min() может показаться чересчур коротким. Как было сказано в разделе 7.3, когда параметром является массив, передается указатель на его первый элемент, первая же размерность фактического аргумента-массива внутри определения функции неизвестна. Чтобы обойти эту трудность, мы объявили первый параметр min() как ссылку на массив, а второй – как его размер. Недостаток подобного подхода в том, что при использовании шаблона с массивами одного и того же типа int, но разных размеров генерируются (или конкретизируются) различные экземпляры функции min().
Имя параметра разрешено употреблять внутри объявления или определения шаблона. Параметр-тип служит спецификатором типа; его можно использовать точно так же, как спецификатор любого встроенного или пользовательского типа, например в объявлении переменных или в операциях приведения типов. Параметр-константа применяется как константное значение – там, где требуются константные выражения, например для задания размера в объявлении массива или в качестве начального значения элемента перечисления.
// size определяет размер параметра-массива и инициализирует
// переменную типа const int
template <class Type, int size>
Type min( const Type (&r_array)[size] )
{
const int loc_size = size;
Type loc_array[loc_size];
// ...
}
Если в глобальной области видимости объявлен объект, функция или тип с тем же именем, что у параметра шаблона, то глобальное имя оказывается скрытым. В следующем примере тип переменной tmp не double, а тот, что у параметра шаблона Type:
typedef double Type;
template <class Type>
Type min( Type a, Type b )
{
// tmp имеет тот же тип, что параметр шаблона Type, а не заданный
// глобальным typedef
Type tm = a < b ? a : b;
return tmp;
}
Объект или тип, объявленные внутри определения шаблона функции, не могут иметь то же имя, что и какой-то из параметров:
template <class Type>
Type min( Type a, Type b )
{
// ошибка: повторное объявление имени Type, совпадающего с именем
// параметра шаблона
typedef double Type;
Type tmp = a < b ? a : b;
return tmp;
}
Имя параметра-типа шаблона можно использовать для задания типа возвращаемого значения:
// правильно: T1 представляет тип значения, возвращаемого min(),
// а T2 и T3 – параметры-типы этой функции
template <class T1, class T2, class T3>
T1 min( T2, T3 );
В одном списке параметров некоторое имя разрешается употреблять только один раз. Например, следующее определение будет помечено как ошибка компиляции:
// ошибка: неправильное повторное использование имени параметра Type
template <class Type, class Type>
Type min( Type, Type );
Однако одно и то же имя можно многократно применять внутри объявления или определения шаблона:
// правильно: повторное использование имени Type внутри шаблона
template <class Type>
Type min( Type, Type );
template <class Type>
Type max( Type, Type );
Имена параметров в объявлении и определении не обязаны совпадать. Так, все три объявления min() относятся к одному и тому же шаблону функции:
// все три объявления min() относятся к одному и тому же шаблону функции
// опережающие объявления шаблона
template <class T> T min( T, T );
template <class U> U min( U, U );
// фактическое определение шаблона
template <class Type>
Type min( Type a, Type b ) { /* ... */ }
Количество появлений одного и того же параметра шаблона в списке параметров функции не ограничено. В следующем примере Type используется для представления двух разных параметров:
#include <vector>
// правильно: Type используется неоднократно в списке параметров шаблона
template <class Type>
Type sum( const vector<Type> &, Type );
Если шаблон функции имеет несколько параметров-типов, то каждому из них должно предшествовать ключевое слово class или typename:
// правильно: ключевые слова typename и class могут перемежаться
template <typename T, class U>
T minus( T*, U );
// ошибка: должно быть <typename T, class U> или
// <typename T, typename U>
template <typename T, U>
T sum( T*, U );
В списке параметров шаблона функции ключевые слова typename и class имеют одинаковый смысл и, следовательно, взаимозаменяемы. Любое из них может использоваться для объявления разных параметров-типов шаблона в одном и том же списке (как было продемонстрировано на примере шаблона функции minus()). Для обозначения параметра-типа более естественно, на первый взгляд, употреблять ключевое слово typename, а не class, ведь оно ясно указывает, что за ним следует имя типа. Однако это слово было добавлено в язык лишь недавно, как часть стандарта C++, поэтому в старых программах вы скорее всего встретите слово class. (Не говоря уже о том, что class короче, чем typename, а человек по природе своей ленив.)
Ключевое слово typename упрощает разбор определений шаблонов. (Мы лишь кратко остановимся на том, зачем оно понадобилось. Желающим узнать об этом подробнее рекомендуем обратиться к книге Страуструпа “Design and Evolution of C++”.)
При таком разборе компилятор должен отличать выражения-типы от тех, которые таковыми не являются; выявить это не всегда возможно. Например, если компилятор встречает в определении шаблона выражение Parm::name и если Parm – это параметр-тип, представляющий класс, то следует ли считать, что name представляет член-тип класса Parm?
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
Parm::name * p; // это объявление указателя или умножение?
// На самом деле умножение
}
Компилятор не знает, является ли name типом, поскольку определение класса, представленного параметром Parm, недоступно до момента конкретизации шаблона. Чтобы такое определение шаблона можно было разобрать, пользователь должен подсказать компилятору, какие выражения включают типы. Для этого служит ключевое слово typename. Например, если мы хотим, чтобы выражение Parm::name в шаблоне функции minus() было именем типа и, следовательно, вся строка трактовалась как объявление указателя, то нужно модифицировать текст следующим образом:
template <class Parm, class U>
Parm minus( Parm* array, U value )
{
typename Parm::name * p; // теперь это объявление указателя
}
Ключевое слово typename используется также в списке параметров шаблона для указания того, что параметр является типом.
Шаблон функции можно объявлять как inline или extern – как и обычную функцию. Спецификатор помещается после списка параметров, а не перед словом template.
// правильно: спецификатор после списка параметров
template <typename Type>
inline
Type min( Type, Type );
// ошибка: спецификатор inline не на месте
inline
template <typename Type>
Type min( Array<Type>, int );
Упражнение 10.1
Определите, какие из данных определений шаблонов функций неправильны. Исправьте ошибки.
(a) template <class T, U, class V>
void foo( T, U, V );
(b) template <class T>
T foo( int *T );
(c) template <class T1, typename T2, class T3>
T1 foo( T2, T3 );
(d) inline template <typename T>
T foo( T, unsigned int* );
(e) template <class myT, class myT>
void foo( myT, myT );
(f) template <class T>
foo( T, T );
(g) typedef char Ctype;
template <class Ctype>
Ctype foo( Ctype a, Ctype b );
Упражнение 10.2
Какие из повторных объявлений шаблонов ошибочны? Почему?
(a) template <class Type>
Type bar( Type, Type );
template <class Type>
Type bar( Type, Type );
(b) template <class T1, class T2>
void bar( T1, T2 );
template <typename C1, typename C2>
void bar( C1, C2 );
Упражнение 10.3
Перепишите функцию putValues() из раздела 7.3.3 в виде шаблона. Параметризуйте его так, чтобы было два параметра шаблона (для типа элементов массива и для размера массива) и один параметр функции, являющийся ссылкой на массив. Напишите определение шаблона функции.