Реализация стека в виде шаблона потребовала некоторых изменений. Во-первых, в стеке с указателями (см. листинг 6.9) методы top() и рор() возвращали нуль (недопустимое значение для указателей), если стек был пуст. Такое решение подходит для указателей, но совершенно не годится для шаблонов — мы не можем знать, какие значения типа Т являются допустимыми, а какие — нет. Поэтому в шаблонном классе вместо возврата значения генерируется исключение Error.
Во-вторых, написан деструктор. И при реализации деструктора, и при реализации методов top () и pop () неявно предполагалось, что класс Т имеет конструктор копирования, так как этот конструктор будет вызываться в деструкторе стека, при возврате значения из методов top() ирор(),а также в теле цикла:
while(!emptyO) { Т t = рор(); }
Кроме того, класс Т должен иметь конструктор по умолчанию, который будет вызываться в методе push () при создании нового элемента стека. Неявные предположения такого рода при создании шаблонных классов программист обязательно должен иметь в виду.
Использование шаблонного стека демонстрирует программа-клиент. Сначала создается стек с числами типа double:
TStack<double> t;
Обнаружив такую запись, компилятор создает из шаблонного класса конкретный класс, подставив на место Т тип double (листинг 11.2). Процесс создания конкретного класса из шаблона путем подстановки аргументов называется ин-станцированием шаблона (см. п. 14.7 в [1]).
ПРИМЕЧАНИЕ
. Термин «инсталлирование» (instantiation) не слишком понятен, хотя стал уже практически общепринятым. К сожалению, именно этот термин использовался при переводе самой важной книги о шаблонах [28] и при переводе «библии по С++» [2]. В русском языке наиболее близко передает смысл этого понятия термин «конкретизация».
Листинг 11.2. Инстанцированный шаблон
class TStack { struct Elem { double data: Elem *next;
Elem (const double& d. Elem *p) :data(d). next(p) { }
};
Elem * Head; int count;
TStack(const TStack &);
TStack& operator=(const TStack &);
public:
class Error: public std::exception { };
TStackO: Head(0), count(0) { }
-TStackO
{ while( lemptyO) pop(); } void push(const double& d) { Head = new Elem(d, Head); ++count;
}
double top() const { if (lemptyO) return Head->dat else throw ErrorO ;
// имя корректируется компилятором
// подставпен тип double
// подставлен тип double
// указатель на вершину
// счетчик элементов
// закрыли копирование
// закрыли присваивание
// исключение - пустой стек
// конструктор
// деструктор!
// подставпен тип double
// новый элемент в стек
// увеличиваем счетчик
II подставлен тип double
Листинг 11.2 (продолжение) void pop()
{ if (emptyO) throw Error();
double top = Head->data; // подставлен тип double
Elem *oldHead = Head; Head = Head->next; delete oldHead;
--count; // уменьшаем счетчик
}
bool emptyO const // есть ли элементы в стеке
{ return Head==0; }
int countO const // количество элементов в стеке
{ return count; }
};
Именно этот класс после подстановки транслируется и попадает в работающую программу. Именно он и используется при объявлении объекта-стека t в программе-клиенте (см. листинг 11.1). Далее выполняется обычная работа с этим стеком, в частности аргументы-числа метода push() переводятся по умолчанию в double.
Обратите внимание, что имя инстанцированного класса выделено курсивом. Дело в том, что компилятор корректирует имя инстанцированного класса аналогично тому, как он поступает с именами функций при перегрузке. Зачем же это нужно? Когда компилятор встречает другое объявление, то снова инстанцируется конкретный класс-стек, например, для работы со строками используется такое объявление:
TStack<string> S;
Очевидно, имена у классов TStack<double> и TStack<string> должны различаться, иначе возникает ошибка повторного определения.
Таким образом, каждое объявление объекта-стека с аргументом-типом приводит к появлению в программе соответствующего инстанцированного класса. Если наследование и композиция служат для многократного использования объектного кода, то шаблоны, как мы видим, позволяют многократно использовать исходные тексты.
В качестве шаблонного типа мы можем задавать и указатели. Например, мы вполне можем написать в программе следующее объявление:
TStack<void *> st;
По этому объявлению создается класс, практически совпадающий с первым универсальным стеком (см. листинг 6.9) — добавлен только деструктор. И работать с таким стеком нужно точно так же, как и с определенным вручную:
st.push(new double(21)); // помещаем в стек числа
st.push(new double(22)) st.push(new double(23))
while (!st.empty()) // пока стек не пустой
{ cout << *(double *)st.top() << endl; // выводим число с вершины
double *р = (double *)st.pop(); // удаляем элемент из стека
delete р; // возвращаем память
|