Определение шаблона класса
Предположим, что нам нужно определить класс, поддерживающий механизм очереди. Очередь– это структура данных для хранения коллекции объектов; они помещаются в конец очереди, а извлекаются из ее начала. Поведение очереди описывают аббревиатурой FIFO – “первым пришел, первым ушел”. (Определенный в стандартной библиотеке C++ тип, реализующий очередь, упоминался в разделе 6.17. В этой главе мы создадим упрощенный тип для знакомства с шаблонами классов.)
Необходимо, чтобы наш класс Queue поддерживал следующие операции:
- добавить элемент в конец очереди:
- удалить элемент из начала очереди:
- определить, пуста ли очередь:
- определить, заполнена ли очередь:
- сам шаблон класса Queue предоставляет описанный выше открытый интерфейс и пару членов: front и back. Очередь реализуется с помощью связанного списка;
- шаблон класса QueueItem представляет один узел связанного списка Queue. Каждый помещаемый в очередь элемент сохраняется в объекте QueueItem, который содержит два члена: value и next. Тип value будет различным в каждом экземпляре класса Queue, а next – это всегда указатель на следующий объект QueueItem в очереди.
void add( item );
item remove();
bool is_empty();
bool is_full();
Определение Queue могло бы выглядеть так:
class Queue {
public:
Queue();
~Queue();
Type& remove();
void add( const Type & );
bool is_empty();
bool is_full();
private:
// ...
};
Вопрос в том, какой тип использовать вместо Type? Предположим, что мы решили реализовать класс Queue, заменив Type на int. Тогда Queue может управлять коллекциями объектов типа int. Если бы понадобилось поместить в очередь объект другого типа, то его пришлось бы преобразовать в тип int, если же это невозможно, компилятор выдаст сообщение об ошибке:
Queue qObj;
string str( "vivisection" );
qObj.add( 3.14159 ); // правильно: в очередь помещен объект 3
qObj.add( str ); // ошибка: нет преобразования из string в int
Поскольку любой объект в коллекции имеет тип int, то язык C++ гарантирует, что в очередь можно поместить либо значение типа int, либо значение, преобразуемое в такой тип. Это подходит, если предстоит работа с очередями объектов только типа int. Если же класс Queue должен поддерживать также коллекции объектов типа double, char, комплексные числа или строки, подобная реализация оказывается слишком ограничительной.
Конечно, эту проблему можно решить, создав копию класса Queue для работы с типом double, затем для работы с комплексными числами, затем со строками и т.д. А поскольку имена классов перегружать нельзя, каждой реализации придется дать уникальное имя: IntQueue, DoubleQueue, ComplexQueue, StringQueue. При необходимости работать с другим классом придется снова копировать, модифицировать и переименовывать.
Такой метод дублирования кода крайне неудобен. Создание различных уникальных имен для Queue представляет лексическую сложность. Имеются и трудности администрирования: любое изменение общего алгоритма придется вносить в каждую реализацию класса. В общем случае процесс ручной генерации копий для индивидуальных типов никогда не кончается и очень сложен с точки зрения сопровождения.
К счастью, механизм шаблонов C++ позволяет автоматически генерировать такие типы. Шаблон класса можно использовать при создании Queue для очереди объектов любого типа. Определение шаблона этого класса могло бы выглядеть следующим образом:
template <class Type>
class Queue {
public:
Queue();
~Queue();
Type& remove();
void add( const Type & );
bool is_empty();
bool is_full();
private:
// ...
};
Чтобы создать классы Queue, способные хранить целые числа, комплексные числа и строки, программисту достаточно написать:
Queue<int> qi;
Queue< complex<double> > qc;
Queue<string> qs;
Реализация Queue представлена в следующих разделах с целью иллюстрации определения и применения шаблонов классов. В реализации используются две абстракции шаблона:
Прежде чем приступать к детальному изучению реализации этих шаблонов, рассмотрим, как они объявляются и определяются. Вот объявление шаблона класса QueueItem:
template <class T>
class QueueItem;
Как объявление, так и определение шаблона всегда начинаются с ключевого слова template. За ним следует заключенный в угловые скобки список параметров шаблона, разделенных запятыми. Список не бывает пустым. В нем могут быть параметры-типы, представляющие некоторый тип, и параметры-константы, представляющие некоторое константное выражение.
Параметр- тип шаблона состоит из ключевого слова class или typename (в списке параметров они эквивалентны), за которым следует идентификатор. (Ключевое слово typename не поддерживается компиляторами, написанными до принятия стандарта C++. В разделе 10.1 подробно объяснялось, зачем это слово было добавлено в язык.) Оба ключевых слова обозначают, что последующее имя параметра относится к встроенному или определенному пользователем типу. Например, в приведенном выше определении шаблона QueueItem имеется один параметр-тип T. Допустимым фактическим аргументом для T является любой встроенный или определенный пользователем тип, такой, как int, double, char*, complex или string.
У шаблона класса может быть несколько параметров-типов:
template <class T1, class T2, class T3>
class Container;
Однако ключевое слово class или typename должно предшествовать каждому. Следующее объявление ошибочно:
// ошибка: должно быть <typename T, class U> или
// <typename T, typename U>
template <typename T, U>
class collection;
Объявленный параметр-тип служит спецификатором типа в оставшейся части определения шаблона и употребляется точно так же, как любой встроенный или определенный пользователем тип в обычном определении класса. Например, параметр-тип можно использовать для объявления данных и функций-членов, членов вложенных классов и т.д.
Не являющийся типом параметр шаблона представляет собой обычное объявление. Он показывает, что следующее за ним имя – это потенциальное значение, употребляемое в определении шаблона в качестве константы. Так, шаблон класса Buffer может иметь параметр-тип, представляющий типы элементов, хранящихся в буфере, и параметр-константу, содержащий его размер:
template <class Type, int size>
class Buffer;
За списком параметров шаблона следует определение или объявление класса. Шаблон определяется так же, как обычный класс, но с указанием параметров:
template <class Type>
class QueueItem {
public:
// ...
private:
// Type представляет тип члена
Type item;
QueueItem *next;
};
В этом примере Type используется для обозначения типа члена item. По ходу выполнения программы вместо Type могут быть подставлены различные встроенные или определенные пользователем типы. Такой процесс подстановки называется конкретизацией шаблона.
Имя параметра шаблона можно употреблять после его объявления и до конца объявления или определения шаблона. Если в глобальной области видимости объявлена переменная с таким же именем, как у параметра шаблона, то это имя будет скрыто. В следующем примере тип item равен не double, а типу параметра:
typedef double Type;
template <class Type>
class QueueItem {
public:
// ...
private:
// тип Item - не double
Type item;
QueueItem *next;
};
Член класса внутри определения шаблона не может быть одноименным его параметру:
template <class Type>
class QueueItem {
public:
// ...
private:
// ошибка: член не может иметь то же имя, что и
// параметр шаблона Type
typedef double Type;
Type item;
QueueItem *next;
};
Имя параметра шаблона может встречаться в списке только один раз. Поэтому следующее объявление компилятор помечает как ошибку:
// ошибка: неправильное использование имени параметра шаблона Type
template <class Type, class Type>
class container;
Такое имя разрешается повторно использовать в объявлениях или определениях других шаблонов:
// правильно: повторное использование имени Type в разных шаблонах
template <class Type>
class QueueItem;
template <class Type>
class Queue;
Имена параметров в опережающем объявлении и последующем определении одного и того же шаблона не обязаны совпадать. Например, все эти объявления QueueItem относятся к одному шаблону класса:
// все три объявления QueueItem
// относятся к одному и тому же шаблону класса
// объявления шаблона
template <class T> class QueueItem;
template <class U> class QueueItem;
// фактическое определение шаблона
template <class Type>
class QueueItem { ... };
У параметров могут быть аргументы по умолчанию (это справедливо как для параметров-типов, так и для параметров-констант) – тип или значение, которые используются в том случае, когда при конкретизации шаблона фактический аргумент не указан. В качестве такого аргумента следует выбирать тип или значение, подходящее для большинства конкретизаций. Например, если при конкретизации шаблона класса Buffer не указан размер буфера, то по умолчанию принимается 1024:
template <class Type, size = 1024>
class Buffer;
В последующих объявлениях шаблона могут быть заданы дополнительные аргументы по умолчанию. Как и в объявлениях функций, если для некоторого параметра задан такой аргумент, то он должен быть задан и для всех параметров, расположенных в списке правее (даже в другом объявлении того же шаблона):
template <class Type, size = 1024>
class Buffer;
// правильно: рассматриваются аргументы по умолчанию из обоих объявлений
template <class Type=string, int size>
class Buffer;
(Отметим, что аргументы по умолчанию для параметров шаблонов не поддерживаются в компиляторах, реализованных до принятия стандарта C++. Чтобы примеры из этой книги, в частности из главы 12, компилировались большинством современных компиляторов, мы не использовали такие аргументы.)
Внутри определения шаблона его имя можно применять как спецификатор типа всюду, где допустимо употребление имени обычного класса. Вот более полная версия определения шаблона QueueItem:
template <class Type>
class QueueItem {
public:
QueueItem( const Type & );
private:
Type item;
QueueItem *next;
};
Обратите внимание, что каждое появление имени QueueItem в определении шаблона – это сокращенная запись для
QueueItem<Type>
Такую сокращенную нотацию можно употреблять только внутри определения QueueItem (и, как мы покажем в следующих разделах, в определениях его членов, которые находятся вне определения шаблона класса). Если QueueItem применяется как спецификатор типа в определении какого-либо другого шаблона, то необходимо задавать полный список параметров. В следующем примере шаблон класса используется в определении шаблона функции display. Здесь за именем шаблона класса QueueItem должны идти параметры, т.е. QueueItem<Type>.
template <class Type>
void display( QueueItem<Type> &qi )
{
QueueItem<Type> *pqi = &qi;
// ...
}