Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

hkjCJgcQqF

.pdf
Скачиваний:
2
Добавлен:
15.04.2023
Размер:
810.94 Кб
Скачать

Глава 4. Шаблоны функций

С помощью шаблонов можно создавать обобщенные функции и классы, которые работают с типом данных, заданным как параметр. Таким образом, одну и ту же функцию или класс можно применять к разным типам данным.

Обобщенные функции

Обобщенная функция определяет универсальную совокупность операций, применяемых к различным типам данных. Тип данных, с которыми работает функция, передается в качестве параметра. Многие алгоритмы носят универсальный характер и не зависят от типа данных, которыми они оперируют. Обобщенная функция объявляется с помощью ключевого слова template. Определение шаблонной функции:

template <class T >

тип_возвращаемого_значения имя_функции (список_параметров)

{

//Тело функции

}

Здесь параметр T задает тип данных, с которым работает функция. При создании конкретной версии обобщенной функции компилятор автоматически подставит вместо него фактический тип. Имена параметров в объявлении и определении не обязаны совпадать. Число появлений одного и того же параметра шаблона в списке параметров функции не ограничено. Если шаблон функции имеет несколько параметров-типов, то каждому из них должно предшествовать ключевое слово class или typename. Шаблон функции можно объявлять как inline или extern – также как и обычную функцию. Спецификатор помещается после списка параметров, а не перед словом template.

// правильно: спецификатор после списка параметров template <typename Type>

inline

Type min( Type, Type );

Традиционно обобщенный тип задается с помощью ключевого слова class, но можно применять вместо него ключевое слово typename.

Примечание. Ключевое слово typename введено в язык в процессе стандартизации C++ для указания того, что идентификатор в шаблоне является типом. Рассмотрим следующий пример: template <typename T>

class MyClass {

typename T::SubType * ptr;

};

Вэтом примере второе ключевое слово typename используется для пояснения, что SubType является типом, определенным внутри класса Т. Таким образом, ptr является указателем на Т:: SubType. Без такого

указания

с

помощью

typename

идентификатор

SubType

103

интерпретировался бы как статический член класса, т.е. как конкретная переменная или объект. В результате выражение T::SubType * ptr представляло бы собой умножение статического члена класса SubType на ptr. В общем случае ключевое слово typename следует использовать всякий раз, когда имя, зависящее от параметра шаблона, представляет собой тип .

Пример шаблонной функции. #include <cstdlib>

#include <iostream> using namespace std; template <class T>

inline T const& max(T const & a, T const& b)

{

return a < b ? b : a;

}

int main(int argc, char *argv[])

{

int i = 42;

cout<<"max(7,i): " <<::max(7,i)<<endl; double fl = 3.4;

double f2 = -6.7;

cout<<" max(f 1, f2) : " <<::max(fl,f2)<<endl; std::string s1 = "mathematics";

std::string s2 = "math" ;

cout << "max(s1,s2): " <<::max(s1,s2)<<endl; system("PAUSE");

return EXIT_SUCCESS;

}

В этой программе max ( ) вызывается трижды: для двух значений типа int, для двух double и для двух std: :string. Каждый раз вычисляется большее значение. В результате программа выводит следующую информацию:

max(7,i): 42 max(fl,f2) : 3.4

max(sl/s2): mathematics

Обратите внимание на то, что в примере каждый вызов шаблона max ( ) предваряется двумя двоеточиями. Делается это потому, что в стандартной библиотеке тоже есть шаблон std: :max (), который может быть вызван при определенных обстоятельствах или способен привести к неоднозначности.

Инстанцирование

Процесс подстановки конкретных типов вместо параметров шаблона называется инстанцированием шаблона (instantiation). Его результатом является экземпляр шаблона. Для запуска процесса инстанцирования достаточно просто использовать шаблон функции. Специально требовать

104

от компилятора инстанцирования шаблона не нужно. Таким образом, шаблоны компилируются дважды. Без инстанцирования код самого шаблона проверяется на правильность синтаксиса. Выявляются синтаксические ошибки, например, пропущенные точки с запятой.

Во время инстанцирования код шаблона проверяется на корректность всех

вызовов.

Выявляются

некорректные

вызовы,

в

частности,

неподдерживаемые вызовы функций.

 

 

 

Здесь

проявляется

важная

проблема,

связанная

с

обработкой

шаблонов:

если

применение

шаблона

функции

 

предполагает

инстанцирование, то компилятору в определенный момент потребуется полное определение этого шаблона. Это отличается от обычных функций, когда для компиляции достаточно их объявления.

Замечание. Автоматическое преобразование типов в шаблонах не допускается. Должно выполняться точное соответствие для каждого параметра типа, например:

template <typename T>

inline T const & max (T const& a, T const& b);

max(4,7); // верно параметр Т - int для обоих аргументов max(4,4.2); // ошибка: первый параметр Т - int, второй - double

Существует несколько способов исправить эту ошибку.

Привести оба аргумента к одному типу: max(static_cast<double>(4) ,4.2) ; //верно

Указать тип Т явно:

max<double>(4,4.2); //верно

• Указать, что параметры могут иметь различные типы.

Общий алгоритм вывода аргументов шаблона можно сформулировать следующим образом:

1.По очереди исследуется каждый фактический аргумент функции, чтобы выяснить, присутствует ли в соответствующем формальном параметре какой-нибудь параметр шаблона.

2.Если параметр шаблона найден, то путем анализа типа фактического аргумента выводится соответствующий аргумент шаблона.

3.Тип фактического аргумента функции не обязан точно соответствовать типу формального параметра. Для приведения типов могут быть применены следующие преобразования:

трансформации - lvalue значения ;

преобразования спецификаторов;

приведение производного класса к базовому при условии, что формальный параметр функции имеет вид T<args>& или T<args>*, где список аргументов args содержит хотя бы один параметр

шаблона.

4. Если один и тот же параметр шаблона найден в нескольких формальных параметрах функций, то аргумент шаблона, выведенный по

105

каждому из соответствующих фактических аргументов, должен быть одним и тем же.

Шаблон конкретизируется либо при вызове функции, либо при взятии её адреса. В следующем примере указатель pf инициализируется адресом конкретизированного экземпляра шаблона. Его аргументы определяются путем исследования типа параметра.

template <typename Type, int size>

Type min( Type (&pArray)[size] ) { /* ... */ } // pf указывает на int min( int (&)[10] ) функции, на которую указывает pf:

int (*pf)(int (&)[10]) = &min;

Тип pf – это указатель на функцию с параметром типа int(&)[10], который определяет тип аргумента шаблона Type и значение аргумента шаблона size при конкретизации min(). Аргумент шаблона Type будет иметь тип int, а значением аргумента шаблона size будет 10. Конкретизированная функция представляется как min(int(&)[10]), и указатель pf адресует именно ее. Когда берется адрес шаблона функции, контекст должен быть таким, чтобы можно было однозначно определить типы и значения аргументов шаблона. Если сделать это не удается, компилятор выдает сообщение об ошибке. Можно использовать явное приведение типов для указания типа аргумента.

Использование строковых литералов в качестве аргументов шаблонов функций.

В том случае, когда шаблон функции имеет параметры ссылочного типа, передача аргументов, являющихся строковыми литералами, может вызвать неожиданные ошибки при работе программ. Рассмотрим пример.

#include <string>

// Обратите внимание на ссылочные параметры template <class T>

inline T const& max(T const& a, T const& b)

{

return a < b ? b : a;

}

int main()

{

std::string s;

::max("яблоко", "персик"); // верно: тип одинаков ::max("яблоко", "груша"); // ошибка: типы разные ::max("яблоко",s); // ошибка: типы разные

}

Проблема заключается в том, что в зависимости от длины строковые литералы имеют разные типы, т.е. являются разными массивами. Другими словами, "яблоко" и "персик"

106

имеют тип char const [7], в то время как "груша"— тип char const [6]. Корректным является только первый вызов функции, поскольку шаблон предполагает, что оба параметра имеют одинаковый тип. Однако если будут объявлены параметры не ссылочного типа, то вместо них можно подставить строковые литералы разного размера.

#include <string>

// Обратите внимание: параметры не ссылочного типа template <typename T>

inline T max(T a, T b) /

{

return a < b ? b : а;

}

int main ()

{

std::string s;

::max("apple","peach"); // верно: тип одинаков ::max("apple","apelcin"); // верно: сведение массивов до одинаковых

типов

::max("apple", s) ; // ошибка : типы разные

}

Объясняется это следующим образом: в процессе вывода аргументов преобразование из массива в указатель (часто называемое сведением (decay)) происходит только в том случае, если параметр имеет не ссылочный тип. Это показано на примере приведенной ниже программы.

#include <cstdlib> #include <iostream> #include <string> #include <typeinfo> using namespace std; template <typename T> void ref(T const & x) {

cout << "x in ref(T const&): " << typeid(x).name()<<endl;

}

template <typename T> void nonref(T x) {

cout<<"x in nonref(T): " << typeid(x).name() <<endl; } int main (int argc, char *argv[]) {

ref("hello");

nonref("hello");

system("PAUSE"); return EXIT_SUCCESS;

}

107

В данном примере аргумент, представляющий собой строковый литерал, передается шаблонам функций, параметры которых объявлены как параметры ссылочного и нессылочного типов соответственно. В обоих шаблонах функции для вывода на экран информации о типах сгенерированных экземпляров параметров используется оператор typeid, который возвращает lvalue типа std:: type_info; это значение инкапсулирует представление типа выражения, передаваемого оператору typeid.

Функция-член name () класса std:: type_infо предназначена для возвращения понятного человеку текстового представления этого типа. На самом деле в стандарте C++ не сказано, что функция name () должна возвращать что-либо осмысленное. Универсального способа решения проблемы несоответствия между массивом символов и указателем на символы не существует. В зависимости от контекста, можно прибегнуть к одному из перечисленных ниже способов:

Использование вместо ссылок значений, не являющихся таковыми (однако это может повлечь за собой лишнее копирование).

Перегрузка, позволяющая использовать как параметры ссылочного типа, так и параметры не ссылочного типа (однако это может привести к неоднозначности.

Перегрузка для конкретных типов данных (таких, как std: : string).

Перегрузка для массивов, например:

template <typename Т, int N, int M>

 

 

T const* max (T const (&a)[N] , T const (&b)[M])

 

{

 

 

 

 

return a < b ? b : a; }

 

 

 

Использование

прикладными

программистами

явного

преобразования типов.

Для данного примера лучше всего подходит перегрузка max () для строк :

// Наибольшая из двух С-строк

inline char const* const& max(char const* const& a, char const* const& b)

{

return std: :strcmp(a,b) < 0 ? b : a; }

Это необходимо в любом случае, поскольку без перегрузки при вызове max () для строковых литералов будет происходить сравнение указателей: сравнение а < b означает сравнение адресов двух строковых литералов, что ничего не дает в плане упорядочения по алфавиту. Это еще одна причина, по которой в большинстве случаев следует отдавать предпочтение строковому классу наподобие std:: string перед строками в С- стиле.

108

Параметры шаблонов

Существуют два вида параметров шаблонов функций. Параметры шаблона, которые объявляются в угловых скобках перед именем шаблона функции:

template <typename T> // Т является параметром шаблона

параметры вызова, которые объявляются в круглых скобках после имени шаблона функции:

max1(T const& a, T const& b); // а и b - параметры вызова Количество задаваемых параметров неограниченно. Однако в шаблонах функций (в отличие от шаблонов классов) нельзя использовать аргументы шаблона по умолчанию. Например, можно определить шаблон max ( ) для двух различных типов данных:

template <class Tl, class T2>

inline Tl max1 (Tl const & a, T2 const & b)

{

return a < b ? b : a;

}

max1(4,4.2)

// верно, однако тип возвращаемого значения определяется типом первого аргумента.

Казалось бы, неплохо иметь возможность передавать шаблону mах ( ) два параметра вызова различных типов, но этот способ имеет свои недостатки. Проблема заключается в том, что требуется объявить тип возвращаемого значения. Если для этого использовать один из типов параметров, аргумент для другого параметра должен конвертироваться в этот же тип, независимо от того, что именно хотел бы получить вызвавший этот шаблон программист.

В C++ нет возможности задать выбор "наиболее мощного типа". Таким образом, в зависимости от порядка аргументов при вызове можно получить наибольшее из значений 42 и 67.65, как double 67. 65, и как int 67. Еще один недостаток заключается в том, что при конвертировании типа второго параметра в тип возвращаемого значения создается новый локальный временный объект, а это означает, что возврат результата по ссылке невозможен. Поэтому в этом примере тип возвращаемого значения должен быть Т1, а не Tl const&. Поскольку типы параметров вызова конструируются из параметров шаблона, параметры шаблона и параметры вызова обычно взаимосвязаны. Эта концепция обеспечивает возможность вызывать шаблонную функцию так же, как и обычную. Однако, можно явно инстанцировать шаблон для конкретных типов:

template <typename T>

inline T const& max1(T const& a, T const& b); max1<double>(4, 4.2);

// Инстанцирование для Т, представляющего собой double.

109

Параметры шаблонов функций, не являющиеся типами

Параметры шаблонов можно разделить на три разновидности:

Параметрытипы;

Параметры, не являющиеся типами;

Шаблонные параметры шаблонов.

Параметры шаблона задаются в начальном параметризованном объявлении шаблона. Такие объявления не обязательно должны быть именованными:

template <typename int> class X;

Однако, если дальше в тексте шаблона имеется ссылка на параметр, то имя параметра, конечно же, необходимо. Заметим также, что имя параметра шаблона может использоваться в последующих объявлениях параметров

(но не в предшествующих).

 

 

template <typename Т>

 

 

// Первый параметр используется в объявлении второго

 

Т* Root ;

 

 

template<T*> class Buf> // и третьего параметров

 

 

class Structure;

 

 

Параметры, не являющиеся типами, можно

использовать

в

шаблонах функций. Например, приведенный ниже

шаблон функции

определяет группу функций, предназначенных для добавления некоторого значения.

template <class Т, int VAL> T addValue (T const& x)

{

return x + VAL;

}

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

std::transform(source.begin(), source.end(), dest.begin(), addValue<int,5>); Последний аргумент вызывает инстанцирование шаблона функции addValue, которая добавляет 5 к целочисленному значению. Полученная функция вызывается для каждого элемента исходной коллекции source, в процессе чего последняя преобразуется в результирующую коллекцию dest. Заметим, что в этом примере проявляется следующая проблема:

addValue<int, 5> — это шаблон функции, который интерпретируется как имя семейства перегруженных функций (даже если это семейство состоит всего из одного элемента). Но некоторые компиляторы не могут использовать семейства перегруженных функций при выводе

110

аргументов шаблона. То есть, аргумент шаблона функции необходимо привести к точному типу.

std::transform(source.begin(), source.end(),// Начало и конец исходной коллекции

dest.begin(), // Начало результирующей коллекции (int(*)(int const&))addValue<int,5>); // Операция

Явное задание аргументов шаблона

Иногда автоматически вывести типы аргументов шаблона невозможно. Если процесс вывода дает два различных типа для одного и того же параметра шаблона, то компилятор сообщает об ошибке – неудачном выводе аргументов. В таких ситуациях приходится подавлять механизм вывода и задавать аргументы явно, указывая их с помощью заключенного в угловые скобки списка разделенных запятыми значений, который следует после имени конкретизируемого шаблона функции. Например, можно ввести третий тип аргумента шаблона, который задает тип значения, возвращаемого функцией.

template < class Tl, class T2, class T3> inline T3 max1(Tl const& a, T2 const& b) ;

Однако вывод аргументов шаблона не работает с возвращаемыми типами , a T3 среди типов параметров вызова функции отсутствует. Следовательно, для определения T3 обычный вывод применить нельзя, а значит, список аргументов шаблона нужно задавать явно, например:

template <typename Tl, typename T2, typename RT> inline T3 max1(Tl const& a, T2 const& b); max1<int,double,double>(4,4.2) // верно, но утомительно

До сих пор рассматривались случаи, когда все аргументы шаблона функции либо задавались явно, либо явно не задавался ни один из них. Существует еще один подход: явно задается только первый аргумент, а остальные определяются при помощи вывода. Общее правило можно сформулировать так: следует явно задавать все типы аргументов, которые нельзя определить неявно. Таким образом, если в примере изменить порядок следования параметров шаблона, то при вызове потребуется указать только тип возвращаемого значения.

template < class T3 , class Tl, class T2> inline T3 max1(Tl const& a, T2 const& b);

max<double>(4,4.2) // верно: тип возвращаемого значения — double В данном примере при вызове max<double> значение T3 явно задается как double, a типы параметров Т1 и Т2 определяются путем вывода из переданных аргументов как int и double.

Явная специализация шаблона

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

111

написать более эффективную функцию, чем конкретизированная по шаблону. А иногда общее определение, предоставляемое шаблоном, для некоторого типа просто не работает. Рассмотрим, например, следующее определение шаблона функции max():

// обобщенное определение шаблона template <class T>

T max1( T t1, T t2 ) { return ( t1 > t2 ? t1 : t2 );

Когда этот шаблон конкретизируется с аргументом типа const char*, то обобщенное определение оказывается семантически некорректным, если мы интерпретируем каждый аргумент как строку символов в смысле языка C, а не как указатель на символ. В этом случае необходимо предоставить специализированное определение для конкретизации шаблона. Явное определение специализации – это такое определение, в котором за ключевым словом template следует пара угловых скобок <>, а за ними– определение специализированного шаблона. Здесь указывается имя шаблона, аргументы, для которых он специализируется, список параметров функции и ее тело. В следующем примере для max(const char*, const char*) определена явная специализация:

return ( strcmp( s1, s2 ) > 0 ? s1 : s2 );

Поскольку имеется явная специализация, шаблон не будет конкретизирован с типом const char* при вызове в программе функции max1(const char*, const char*). При любом обращении к max() с двумя аргументами типа const char* работает специализированное определение. Для любых других обращений функция сначала конкретизируется по обобщенному определению шаблона, а затем вызывается. Вот как это выглядит:

//обобщенное определение шаблона template <class T>

T max1( T t1, T t2 ) { return ( t1 > t2 ? t1 : t2 );

//явная специализация для const char*:

//имеет приоритет над конкретизацией шаблона

//по обобщенному определению

//typedef const char *PCC; можно использовать другое имя template<> const char* max1< const char *>(const char* s1,const char* s2 )

{ //определена явная специализация: return ( strcmp( s1, s2 ) > 0 ? s1 : s2 );}

При любом обращении к max() с двумя аргументами типа const char* работает специализированное определение. Для любых других обращений функция сначала конкретизируется по обобщенному определению шаблона, а затем вызывается.

Пример,

#include <iostream>

112

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]