6.1.1. Переопределение шаблонов
Каждая версия функции, генерируемая с помощью шаблона, содержит одинаковый базовый код. Единственным изменяемым свойством функции будет значение параметра (или параметров) типа. Однако дня отдельного параметра (или параметров) типа можно обеспечить специальную обработку. Для этого определяется обычная функция языка C++ с тем же именем, что и шаблон функции, но использующая уже имеющиеся типы данных, а не параметры типов. Обычная функция переопределяет шаблон. Т.е., если при интерпретации вызова компилятор обнаруживает, что типы переданных параметров соответствуют спецификации обычной функции, то он вызовет ее, а не сгенерирует функцию по шаблону.
В качестве примера можно определить новую версию функции Мах, которая будет работать с экземплярами класса CCurrency, описанного в параграфе «Перегрузка операторов» гл. 5. Вспомним: объект класса CCurrency хранит денежную сумму в виде числа долларов и числа центов. Очевидно, что код, определенный в шаблоне Мах, не подходит для сравнения денежных величин, хранимых в двух объектах CCurrency. Следовательно, приведенную ниже версию функции Мах можно включить в программу дополнительно к уже имеющемуся шаблону Мах.
CCurrency Max (CCurrency A, CCurrency В)
{
long DollarsA, DollarsB;
int CentsA, CentsB;
A.GetAmount (SDollarsA, SCentsA);
B.GetAmount UDollarsB, &CentsB);
if (DollarsA > DollarsB || DollarsA == DollarsB && CentsA > CentsB)
return A;
else
return B;
}
Если после этого программа вызовет функцию Мах, передав ей два объекта класса CCurrency, компилятор инициирует вызов приведенной выше функции вместо создания экземпляра функции по шаблону Мах. Например:
CCurrency Bucks1 (29, 95);
CCurrency Bucks2 (31, 47);
Max (Bucks1, Bucks2}.PrintAmount ();
Результатом будет
$31.47
Совет
Вместо переопределения функции Мах для сравнения двух объектов CCurrency можно перегрузить оператор ">", чтобы корректно сравнивать значения денежных сумм, сохраняемых в этих двух объектах. Тогда приведенный в примере шаблон Мах, использующий оператор ">", будет корректно обрабатывать объекты класса CCurrency. Описание перегруженных операторов см. в гл. 5.
6.2. Определение шаблонов классов
В гл. 4 показано, как создание нового производного класса из существующего позволяет повторно использовать код, избегая при этом ненужного дублирования. Допустим, вы сконструировали класс для хранения списка 100 целочисленных значений. Базовое определение класса может быть следующим.
class IntList
{
public:
IntList ();
int Setltem (int Index, const int &Item);
int Getltem (int Index, int &Item);
private:
int Buffer [100];
};
Целочисленные значения хранятся в закрытом массиве Buffer, а специально написанный конструктор по умолчанию инициализирует нулями все элементы этого массива. Функции-члены SetItem и GetItem используются для присваивания или получения значений указанных элементов.
Допустим, нужно получить подобный класс для хранения списка вещественных значений, структур или большого количества элементов (например, 250). В этих случаях определяется полностью новый класс. Создание производного класса от IntList не является решением задачи, так как необходимо не просто добавить несколько новых свойств, а изменить основные типы или константы, на которых основывается построение класса.
При создании программы можно написать единственный шаблон класса, который будет использоваться для автоматической генерации целого семейства взаимосвязанных классов, предназначенных для хранения данных различных типов и различного числа элементов. Вот простая версия такого класса.
template <class T, int I> class CList
{
public:
int SetItem (int Index, const Т &Item);
int GetItem (int Index, Т &Item);
private:
Т Buffer [I];
};
В этом определении T является параметром типа, а I – параметром-константой (точнее, в данном примере – это параметр-константа типа int). Как вы вскоре увидите, фактические значения для параметров T и I устанавливаются при создании определенного экземпляра класса. В его шаблон класса можно включить список из любого числа параметров, ограниченный символами "<" ">" (список должен содержать хотя бы один параметр). Параметры-константы могут иметь любой допустимый тип (не обязательно int, как в приведенном выше примере). Внутри определения класса параметр типа может находиться в любом месте программы, в котором допустимо использование спецификации типа, а параметр-константа – в любом месте программы, в котором допустимо применение константного выражения описанного типа (в нашем примере int).
Функцию-член SetItem можно определить следующим образом.
template<class Т,int I> int CList<T,I>::Setltem(int Index,const Т &Item)
{
if (Index < 0 || Index > I - 1)
return 0; // ошибка
Buffer [Index] = Item;
return 1; // успешное завершение
}
Обратите внимание: функция возвращает значение 1 при удачном завершении или 0, если заданное значение индекса недопустимо.
Как показано в этом примере, реализация функции, помещенной вне определения шаблона, должна содержать следующие два компонента (в дополнение к компонентам, обычно включаемым в определение функции-члена). Кроме того, необходимо отметить следующее.
-
Определение должно начинаться спецификатором template, за которым следует такой же список параметров в угловых скобках, как и в определении шаблона класса (в приведенном примере template <class Т, int I>).
-
За именем класса, предшествующим операции расширения области видимости, должен следовать список имен параметров шаблона (в нашем примере – CList <T, I>). Список используется для полного определения типа класса, к которому принадлежит функция.
Совет
Функция-член шаблона может быть реализована вне его определения. Однако эта реализация должна быть включена в каждый исходный файл программы, содержащий вызов функции. Тогда компилятор сможет сгенерировать код функции из ее определения. В программу с несколькими исходными файлами можно ввести как определение шаблона, так и определения всех его методов в одном файле заголовков, входящем во все исходные файлы. Для функций-членов шаблона класса включение определения функции в несколько исходных файлов не приводит к возникновению ошибки компоновщика "multiply definition symbol" (многократное определение символического имени).
По аналогии метод Getitem можно определить следующим образом.
template <class Т, int I> int CList <T, I>::GetItem (int Index, Т &Item)
{
if (Index < 0 || Index > I - 1)
return 0;
Item = Buffer [Index];
return 1;
}
Примечание
В определении шаблона класса идентификатор i может использоваться как значение размерности массива, поскольку он является параметром-константой шаблона, а не обычным параметром или переменной, которые нельзя использовать для задания размерности массива (язык C++ не позволяет динамически изменять размерность массива во время выполнения программы), Использование параметра-константы допустимо на этапе компиляции, так как при запуске программы значение этого параметра становится константой.
