Внимание!!!
При использовании перегруженных функций нужно с осторожностью задавать начальные значения их параметров. Предположим, мы следующим образом определили перегруженную функцию умножения разного количества параметров:
double multy (double x) { return x * х * х; } double multy (double x, double у) { return x * у * у; } double multy (double x, double у, double z) { return x * у * z; } |
Каждое из следующих обращений к функции multy() будет однозначно идентифицировано и правильно обработано:
multy (0.4) multy (4.0, 12.3) multy (0.1, 1.2, 6.4) |
Однако, добавление в программу функции с начальными значениями параметров:
double multy (double a = 1.0, double b = 1.0, double с = 1.0, double d = 1.0) { return a * b + c * d; } |
навсегда запутает любой компилятор при попытках обработать, например, такой вызов:
multy(0.1, 1.2); |
что приведет к ошибке на этапе компиляции. Будьте внимательны!!!
Следующий раздел урока расскажет вам об альтернативном решении, позволяющем создать универсальную функцию.
Шаблоны функций.
Современные компиляторы C++ реализуют одно из новейших добавлений к языку: шаблоны функций. Шаблон функции — это обобщенное описание функции; т.е. он определяет функцию в терминах обобщенного типа, вместо которого может быть подставлен определенный тип данных, такой как int или double. Передавая шаблону тип в качестве параметра, можно заставить компилятор сгенерировать функцию для этого конкретного типа. Поскольку шаблоны позволяют программировать в терминах обобщенного, а не специфического типа, этот процесс иногда называют обобщенным программированием. Поскольку типы представлены параметрами, средство шаблонов иногда называют параметризироѳанными типами. Давайте посмотрим, чем это средство полезно, и как оно работает.
Шаблоны функций позволяют определять функции в терминах некоторого произвольного типа. Например, можно создать шаблон для осуществления обмена значениями, подобный приведенному ниже:
template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = а;
а = b;
b = temp;
}
Первая строка указывает, что устанавливается шаблон, а произвольный тип данных получает имя AnyType. Ключевые слова template и typename являются обязательными; при этом вместо typename можно использовать ключевое слово class. Кроме того, должны присутствовать угловые скобки. Имя типа может быть любым (в этом примере — AnyType), при условии, что оно соответствует обычным правилам именования C++; многие программисты используют простые имена, такие как Т. Остальная часть кода описывает алгоритм обмена значениями типа AnyType. Никаких функций шаблон не создает. Вместо этого он предоставляет компилятору указания по определению функции. Если необходима функция для обмена значениями int, компилятор создаст функцию согласно этому шаблону, подставляя int вместо АпуТуре. Аналогично, когда нужна функция для обмена значениями double, компилятор будет руководствоваться шаблоном, подставляя тип double вместо АпуТуре.
Совет
Шаблоны должны использоваться в тех случаях, когда необходимы функции, применяющие один и тот же алгоритм к различным типам данных. Если перед вами не стоит задача обеспечения обратной совместимости и не затрудняет набор более длинного слова, можете использовать при объявлении параметров типа ключевое слово typename, а не class.
#include <iostream>
// Прототип шаблона функции
template <typename T> // или class Т
void Swap(T &а, Т &Ь) ;
int main()
{
using namespace std;
int i = 10;
int j = 20;
cout « "i, j = " « i « ", " « j « "An";
cout « "Using compiler-generated int swapper:\n";
Swap(i,j); // генерирует void Swap(int &, int &)
cout << "Now i, j = " <<i<<", " << j << " .\n";
double x = 24.5;
double у = 81.7;
cout « "x, у = " « x « ", " << у « ".\n";
cout « "Using compiler-generated double swapper:\n";
Swap(x,y); // генерирует void Swap(double &, double &)
cout << "Now x, y="<<x<<", "<<y<< M.\n";
// cin.get ();
return 0;
}
// Определение шаблона функции
template <typename T> // или class T
void Swap(T &a, T &b)
{
T temp; // temp - переменная типа Т
temp = a;
a = b;
b = temp;
}
Первая функция Swap() в листинге имеет два аргумента типа int, поэтому компилятор генерирует версию функции, предназначенную для обработки данных типа int. Другими словами, он заменяет каждое использование т типом int, создавая определение следующего вида:
void Swap (int &a, int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
Вы не увидите этот код, но компилятор генерирует его, а затем использует в программе. Вторая функция Swap () имеет два аргумента типа double, поэтому компилятор генерирует версию функции, предназначенную для обработки данных double. Таким образом, он заменяет все вхождения т типом double, генерируя следующий код:
void Swap(double &a, double &b)
{
double temp;
temp = a;
a = b;
b = temp;
}
Обратите внимание, что шаблоны функций не сокращают размеры исполняемых файлов. В листинге все по-прежнему завершается двумя отдельными определениями функций, как если бы это было реализовано вручную. Окончательный код не содержит шаблонов, а содержит реальные функции, сгенерированные для программы. Преимущество шаблонов состоит в упрощении процесса генерации нескольких определений функции, а также в увеличении его надежности.
Чаще всего шаблоны помещаются в заголовочный файл, который затем включается в использующий эти шаблоны файл.
Перегруженные шаблоны
Шаблоны используются, когда необходимо создать функции, которые применяют один и тот же алгоритм к различным типам, как было показано в листинге. Однако, возможно, не для всех типов этот алгоритм выглядит совершенно одинаково. В таких случаях можно перегрузить определения шаблонов, точно так же, как перегружаются обычные функции. Как и при перегрузке функций, перегруженные шаблоны должны иметь различные сигнатуры. Например, в листинг добавлен новый шаблон для обмена элементами между двумя массивами. Исходный шаблон имеет сигнатуру (Т &, Т &), в то время как новый шаблон — сигнатуру (Т [ ], Т [ ] , int). Обратите внимание, что последним аргументом является конкретный тип (int), а не обобщенный. Не все аргументы шаблонов обязательно должны иметь обобщенный тип.
Ограничения шаблонов
Предположим, что имеется следующий шаблон функции:
template <class T> // или template <typename T>
void f (Т а, Т b)
{...}
Часто в коде делаются предположения относительно того, как операции возможны для того или иного типа. Например, в следующем операторе предполагается, что определена операция присваивания, но это не будет верно, если типом Т является встроенный массив:
а = b;
Подобным же образом, ниже предполагается, что определена операция >, что не будет справедливо, если типом Т окажется обычная структура:
if (а > b)
Также хотя операция > определена для имен массивов, поскольку они являются адресами, данная операция сравнивает адреса массивов, а это может быть не тем, что имело в виду. В приведенном ниже операторе предполагается, что для типа Т определена операция умножения, а это не так в случае, когда Т — массив, указатель или структура:
Т с = а* b;
Какую версию функции выбирает компилятор?
В отношении перегрузки функций, шаблонов функций и перегрузки шаблонов функций язык C++ располагает четко определенной стратегией выбора определения функции, применяемого для данного вызова функции, особенно при наличии множества аргументов. Этот процесс выбора называется разрешением перегрузки. Детальное описание стратегии требует отдельной главы, поэтому здесь мы рассмотрим работу этого процесса в общих чертах.
• Фаза 1. Составьте список функций-кандидатов. Таковыми являются функции и шаблоны функций с таким же именем, как у вызываемой функции.
• Фаза 2. Беря за основу список функций-кандидатов, составьте список подходящих функций. Таковыми являются функции с корректным количеством аргументов, для которых существует неявная последовательность преобразований типов. Она включает случай точного совпадения типа каждого фактического аргумента с типом соответствующего формального аргумента. Например, при вызове функции с аргументом типа float это значение может быть приведено к типу double для соответствия типу double формального параметра, а шаблон может сгенерировать экземпляр функции для типа float.
• Фаза 3. Проверьте наличие наиболее подходящей функции. Если она есть, используйте ее. В противном случае вызов функции является ошибочным.
Шаблоны функций в языке С позволяют создать общее определение функции, применяемой для различных типов данных.
В прошлой теме, чтобы использовать одну и ту же функцию с различными типами данных мы создавали отдельную перегруженную версию этой функции для каждого типа. Например:
int Abs(int N) { return N < 0 ? -N : N; }
double Abs(double N) { return N < 0. ? -N : N; } |
Теперь, используя шаблон мы сможем реализовать единственное описание, обрабатывающее значения любого типа:
template <typename T> T Abs (T N) { return N < 0 ? -N : N; } |
Теперь - обсудим то, что у нас получилось.
1. Идентификатор T является параметром типа. Именно он определяет тип параметра, передаваемого в момент вызова функции.
2. Допустим, программа вызывает функцию Abs и передает ей значения типа int:
cout << "Result - 5 = " << Abs(-5); |
3. В данном случае, компилятор автоматически создает версию функции, где вместо T подставляется тип int.
4. Теперь функция будет выглядеть так:
int Abs (int N) { return N < 0 ? -N : N; } |
5. Следует отметить, что компилятор создаст версии функции для любого вызова, с любым типом данных. Такой процесс носит название - создание экземпляра шаблона функции. Или, если склоняться к более специфической терминологии - инстанционирование шаблона (template instantiation).
