class List { class Node{ public:
TData d;
Node *next;
Node *prev;
Node(TData dat = 0) {d = dat; next = 0; prev = 0;}
};
} template <описание_параметров_шаблона> class имя
{ /* определение класса */ };
69
Класс
TData можно рассматривать как параметр, на место которого при компиляции будет подставлен конкретный тип данных. Получившийся шаблонный класс имеет тип
List
Методы шаблона класса автоматически становятся шаблонами функций.
Если метод описывается вне шаблона, его заголовок должен иметь следую- щие элементы: template <описание_параметров_шаблона> возвр_тип имя_класса <параметры_шаблона >:: имя_функции
(список_параметров функции)
Проще рассмотреть синтаксис описания методов шаблона на примере: template void List ::print()
{ /* тело функции */ }
Описание параметров шаблона в заголовке функции должно соответ- ствовать шаблону класса.
Локальные классы не могут иметь шаблоны в качестве своих элемен- тов.
Шаблоны методов не могут быть виртуальными.
Шаблоны классов могут содержать статические элементы, друже- ственные функции и классы.
Шаблоны могут быть производными как от шаблонов, так и от обыч- ных классов, а также являться базовыми и для шаблонов, и для обычных классов.
Внутри шаблона нельзя определять friend
-шаблоны.
Если у шаблона несколько параметров, они перечисляются через запя- тую. Ключевое слово class требуется записывать перед каждым парамет- ром, например: template struct Pair { T1 first; T2 second; };
Параметрам шаблонного класса можно присваивать значения по умол- чанию, они записываются после знака
=
. Как и для обычных функций, зада- вать значения по умолчанию следует, начиная с правых параметров.
Ниже приведено полное описание параметризованного класса двусвяз- ного списка
List template class List
{ class Node
{ public:
TData d;
70
Node *next, *prev;
Node(TData dat = 0){d = dat; next = 0; prev = 0;}
};
Node *pbeg, *pend; public:
List(){pbeg = 0; pend = 0;}
List(); void add(TData d);
Node * find(TData i);
Node * insert(TData key, TData d); bool remove(TData key); void print(); void print_back();
}; template List ::List()
{ if (pbeg !=0)
{
Node *pv = pbeg; while (pv)
{pv = pv->next; delete pbeg; pbeg = pv;}
}
} template void List ::print()
{
Node *pv = pbeg; cout << endl << "list: "; while (pv)
{ cout << pv->d << ' '; pv = pv->next;
} cout << endl;
} template void List ::print_back()
{
Node *pv = pend; cout << endl << " list back: "; while (pv)
{ cout << pv->d << ' '; pv = pv->prev;
} cout << endl;
} template void List ::add(TData d)
{
Node *pv = new Node(d); if (pbeg == 0)pbeg = pend = pv; else
{ pv->prev = pend; pend->next = pv; pend = pv;
}
} template Node * List ::find( TData d)
71
{
Node *pv = pbeg; while (pv)
{ if(pv->d == d) break; pv = pv->next;
} return pv;
} template Node * List ::insert(TData key, TData d)
{ if(Node *pkey = find(key))
{
Node *pv = new Node(d); pv->next = pkey->next; pv->prev = pkey; pkey->next = pv; if( pkey != pend)(pv->next)->prev = pv; else pend = pv; return pv;
} return 0;
} template bool List ::remove(TData key)
{ if(Node *pkey = find(key))
{ if (pkey == pbeg)
{ pbeg = pbeg->next; pbeg->prev = 0;
} else if (pkey == pend)
{ pend = pend->prev; pend->next = 0;
} else
{
(pkey->prev)->next = pkey->next;
(pkey->next)->prev = pkey->prev;
} delete pkey; return true;
} return false;
}
Если требуется использовать шаблон
List для хранения данных не встроенного, а определенного пользователем типа, в его описание необхо- димо добавить перегрузку операции вывода в поток и сравнения на равен- ство, а если для его полей используется динамическое выделение памяти, то и операцию присваивания.
При определении синтаксиса шаблона было сказано, что в него, кроме типов, могут передаваться константы. Соответствующим параметром шаб- лона может быть:
72
переменная целого, символьного, булевского или перечислимого типа;
указатель на объект или указатель на функцию;
ссылка на объект или ссылка на функцию;
указатель на элемент класса.
В теле шаблона такие параметры могут применяться в любом месте, где допустимо использовать константное выражение.
В качестве примера создадим шаблон класса, содержащего блок памяти определенной длины и типа: template
class Block
{ public:
Block(){p = new Type [kol];}
Block(){delete [] p;} operator Type *(); protected:
Type * p;
}; template
Block :: operator Type *()
{ return p;
}
У класса-шаблона могут быть друзья, и шаблоны тоже могут быть дру- зьями. Класс может быть объявлен внутри шаблона, а шаблон – внутри как класса, так и шаблона. Единственным ограничением является то, что шаб- лонный класс нельзя объявлять внутри функции. В любом классе, как в обычном, так и в шаблоне, можно объявить метод-шаблон. После создания и отладки шаблоны классов удобно помещать в заголовочные файлы.
9.3
Использование шаблонов классов
Чтобы создать при помощи шаблона конкретный объект конкретного класса, при описании объекта после имени шаблона в угловых скобках пе- речисляются его фактические параметры: имя_шаблона <фактические параметры> имя_объекта
(параметры_конструктора);
Процесс создания конкретного класса из шаблона путем подстановки ар- гументов называется инстанцированием шаблона. Имя шаблона вместе с фактическими параметрами можно воспринимать как уточненное имя класса. Примеры создания объектов по шаблонам:
List List_int; // список целых чисел
List List_double; // список вещественных чисел
73
List List_monster; // список объектов класса monster
Block buf; // блок символов
Block stado; // блок объектов класса monster
Pair a; // объявление пары целых
Pair b; // объявление пары "целый, вещественный"
Pair b = { 1, 2.1 }; // объявление с инициализацией
Pair d; // аргументы - пользовательские классы
При использовании параметров шаблона по умолчанию список аргумен- тов может оказаться пустым, при этом угловые скобки опускать нельзя: template class String;
String<>* p;
Для каждого инстанцированного класса компилятор создает имя, отли- чающееся и от имени шаблона, и от имен других инстанцированных клас- сов. Тем самым каждый инстанцированный класс определяет отдельный тип. В классе-шаблоне разрешено объявлять статические методы и статиче- ские поля, однако следует учесть, что каждый инстанцированный класс об- ладает собственной копией статических элементов.
После создания объектов с ними можно работать так же, как с объектами обычных классов, например: for (int i = 1; i<10; i++)List_double.add(i*0.08);
List_double.print();
//---------------------------------- for (int i = 1; i<10; i++)List_monster.add(i);
List_monster.print();
//---------------------------------- strcpy(buf, "Очень важное сообщение"); cout << buf << endl;
Для упрощения использования шаблонов классов можно применить пе- реименование типов с помощью typedef
: typedef List Ldbl;
Ldbl List_double;
9.4
Явная специализация шаблонов
Специализация шаблонов является одной из нетривиальных возможно- стей языка C++ и использутся в основном при создании библиотек. Предпо- ложим, что мы хотим изменить шаблон класса
List только для параметров типа int
, тогда необходимо задать явную специализацию шаблона: template<> class List { ... }; // int – аргумент шаблона.
Теперь можно писать методы и поля специальной реализации для int
Такая специализация обычно называется полной специализацией (full
74 specialization или explicit specialization). Для большинства практических за- дач большего не требуется.
Согласно ISO стандарту С++, создав специализированный шаблонный класс мы создаем новый шаблонный класс. Специализированный шаблон- ный класс может содержать методы, поля или объявления типов которых нет в шаблонном классе который мы специализируем. Удобно, когда нужно чтобы метод шаблонного класса работал только для конкретной специали- зации – достаточно объявить метод только в этой специализации, остальное сделает компилятор: template<> class List { void removeAllLessThan(int n); // метод доступен только для
List
};
Из того, что специализированный шаблонный класс это совсем-совсем новый и отдельный класс, следует, что он может иметь отдельные, никак не связанные с неспециализированным шаблонным классом параметры: template< typename T, typename S > class B {}; template< typename U > class B< int, U > {};
Такая специализация шаблона, при которой задается новый список пара- метров и через эти параметры задаются аргументы для специализации назы- вается частичной специализацией (partial specialization).
9.5
Достоинства и недостатки шаблонов
Шаблоны представляют собой мощное и эффективное средство обраще- ния с различными типами данных, которое можно назвать параметрическим полиморфизмом, обеспечивают безопасное использование типов, в отличие от макросов препроцессора, и являются вкупе с шаблонами функций сред- ством реализации идей обобщенного программирования и метапрограмми- рования. Однако следует иметь в виду, что эти средства предназначены для грамотного использования и требуют знания многих тонкостей. Программа, использующая шаблоны, содержит код для каждого порожденного типа, что может увеличить размер исполняемого файла. Кроме того, с одними типами данных шаблоны могут работать не так эффективно, как с другими. В этом случае имеет смысл использовать специализацию шаблона.
Стандартная библиотека С++ предоставляет большой набор шаблонов для различных способов организации хранения и обработки данных.
75
10. ОБРАБОТКА ИСКЛЮЧЕНИЙОбработка исключений – это механизм, позволяющий двум независимо разработанным программным компонентам взаимодействовать в аномаль- ной ситуации, называемой исключением. В
этой главе мы расскажем, как генерировать, или возбуждать, исключение в том месте программы, где имеет место аномалия. Затем мы покажем, как связать catch
-обработчик исключений с множеством инструкций программы, используя try
-блок.
Потом речь пойдет о спецификации исключений – механизме, с помощью которого можно связать список исключений с объявлением функции, и функция не сможет возбудить никаких других исключений. Закончится эта глава обсуждением решений, принимаемых при проектировании про- граммы, в которой используются исключения.
Исключение – это аномальное поведение во время выполнения, которое программа может обнаружить, например: деление на 0, выход за границы массива или истощение свободной памяти. Такие исключения нарушают нормальный ход работы программы, и на них нужно немедленно отреаги- ровать. В C++ имеются встроенные средства для их возбуждения и обра- ботки. С помощью этих средств активизируется механизм, позволяющий двум несвязанным (или независимо разработанным) фрагментам про- граммы обмениваться информацией об исключении.
Когда встречается аномальная ситуация, та часть программы, которая ее обнаружила, может сгенерировать, или возбудить, исключение. Чтобы по- нять, как это происходит, реализуем по-новому класс iStack
, используя ис- ключения для извещения об ошибках при работе со стеком. Определение класса:
#include class iStack { public: iStack( int capacity )
: _stack( capacity ), _top( 0 ) { } bool pop( int &top_value ); bool push( int value ); bool full(); bool empty(); void display(); int size(); private: int _top; vector< int > _stack;
};
76
Стек реализован на основе вектора из элементов типа int
. При создании объекта класса iStack его конструктор создает вектор из int
, размер кото- рого (максимальное
число элементов, хранящихся в стеке) задается с помо- щью начального значения. Например, следующая инструкция создает объ- ект myStack
, который способен содержать не более 20 элементов типа int
: iStack myStack(20);
При манипуляциях с объектом myStack могут возникнуть две ошибки:
запрашивается операция pop()
, но стек пуст;
операция push()
, но стек полон.
Вызвавшую функцию нужно уведомить об этих ошибках посредством исключений. С чего же начать?
Во-первых, мы должны определить, какие именно исключения могут быть возбуждены. В C++ они чаще всего реализуются с помощью классов.
Мы определим два из них, чтобы использовать их как исключения для класса iStack
. Эти определения мы поместим в заголовочный файл stackExcp.h:
// stackExcp.h class popOnEmpty { /* ... */ }; class pushOnFull { /* ... */ };
Затем надо изменить определения функций-членов pop()
и push()
так, чтобы они возбуждали эти исключения. Для этого предназначена инструк- ция throw
, которая во многих отношениях напоминает return
. Она состоит из ключевого слова throw
, за которым следует выражение того же типа, что и тип возбуждаемого исключения. Как выглядит инструкция throw для функции pop()
? Попробуем такой вариант:
// увы, это не совсем правильно throw popOnEmpty;
К сожалению, так нельзя. Исключение – это объект, и функция pop()
должна генерировать объект класса соответствующего типа. Выражение в инструкции throw не может быть просто типом. Для создания нужного объ- екта необходимо вызвать конструктор класса. Инструкция throw для функ- ции pop() будет выглядеть так:
// инструкция является вызовом конструктора throw popOnEmpty();
Эта инструкция создает объект исключения типа popOnEmpty
77
Напомним, что функции-члены pop()
и push()
были определены как возвращающие значение типа bool
:
true означало, что операция заверши- лась успешно, а false
– что произошла ошибка. Поскольку теперь для из- вещения о неудаче pop()
и push()
используют исключения, возвращать значение необязательно. Поэтому мы будем считать, что эти функции- члены имеют тип void
: class iStack
{ public:
// ...
// больше не возвращают значения void pop( int &value ); void push( int value ); private:
// ...
};
Теперь функции, пользующиеся нашим классом iStack
, будут предпо- лагать, что все хорошо, если только не возбуждено исключение; им больше не надо проверять возвращенное значение, чтобы узнать, как завершилась операция. В двух следующих разделах мы покажем, как определить функ- цию для обработки исключений, а сейчас представим новые реализации функций-членов pop()
и push() класса iStack
:
#include "stackExcp.h" void iStack::pop( int &top_value )
{ if ( empty() ) throw popOnEmpty(); top_value = _stack[ --_top ]; cout << "iStack::pop(): " << top_value " endl;
} void iStack::push( int value )
{ cout << "iStack::push( " << value << " )\n"; if ( full() ) throw pushOnFull( value );
_stack[ _top++ ] = value;
}
Хотя исключения чаще всего представляют собой объекты типа класса, инструкция throw может генерировать объекты любого типа. Например, функция mathFunc()
в следующем примере возбуждает исключение в виде объекта-перечисления . Это корректный код C++:
78 enum EHstate { noErr, zeroOp, negativeOp, severeError }; int mathFunc( int i ) { if ( i == 0 ) throw zeroOp; // исключение в виде объекта-перечисления
// в противном случае продолжается нормальная обработка
}
В нашей программе тестируется определенный в предыдущем разделе класс iStack и его функции-члены pop()
и push()
. Выполняется 50 итера- ций цикла for
. На
каждой итерации в стек помещается значение, кратное 3:
3, 6, 9 и т.д. Если значение кратно 4 (4, 8, 12...), то выводится текущее со- держимое стека, а если кратно 10 (10, 20, 30...), то с вершины снимается один элемент, после чего содержимое стека выводится снова. Как нужно из- менить функцию main()
, чтобы она обрабатывала исключения, возбуждае- мые функциями-членами класса iStack
?
#include
#include "iStack.h" int main() { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix )
{ if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display();
}
} return 0;
}
Инструкции, которые могут возбуждать исключения, должны быть за- ключены в try- блок. Такой блок начинается с ключевого слова try
, за ко- торым идет последовательность инструкций, заключенная в фигурные скобки, а после этого – список обработчиков, называемых catch- предложе- ниями.
Try
-блок группирует инструкции программы и ассоциирует с ними обработчики исключений. Куда нужно поместить try
-блоки в функции main()
, чтобы были обработаны исключения popOnEmpty и pushOnFull
? for ( int ix = 1; ix < 51; ++ix ) {
79 try { // try-блок для исключений pushOnFull if ( ix % 3 == 0 ) stack.push( ix );
} catch ( pusOnFull ) { ... } if ( ix % 4 == 0 ) stack.display(); try { // try-блок для исключений popOnEmpty if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display();
}
} catch ( popOnEmpty ) { ... }
}
В таком виде программа выполняется корректно. Однако обработка ис- ключений в ней перемежается с кодом, использующимся при нормальных обстоятельствах, а такая организация несовершенна. В конце концов, ис- ключения – это аномальные ситуации, возникающие только в особых слу- чаях. Желательно отделить код для обработки аномалий от кода, реализую- щего операции со стеком. Мы полагаем, что показанная ниже схема облег- чает чтение и сопровождение программы: try { for ( int ix = 1; ix < 51; ++ix )
{ if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display();
}
}
} catch ( pushOnFull ) { ... } catch ( popOnEmpty ) { ... }
С try
-блоком ассоциированы два catch
-предложения, которые могут обработать исключения pushOnFull и popOnEmpty
, возбуждаемые функци- ями-членами push()
и pop()
внутри этого блока. Каждый catch- обработ- чик определяет тип «своего» исключения. Код для обработки исключения помещается внутрь составной инструкции (между фигурными скобками), которая является частью catch- обработчика. (Подробнее catch
-предложе- ния мы рассмотрим в следующем разделе.)
Исполнение программы может пойти по одному из следующих путей:
80
если исключение не возбуждено, то выполняется код внутри try
-блока, а ассоциированные с ним обработчики игнорируются. Функция main()
возвращает 0;
если функция-член push()
, вызванная из первой инструкции if внутри цикла for
, возбуждает исключение, то вторая и третья инструкции if игнорируются, управление покидает цикл for и try
-блок, и выполняется обработчик исключений типа pushOnFull
;
если функция-член pop()
, вызванная из третьей инструкции if внутри цикла for
, возбуждает исключение, то вызов display()
игнориру- ется, управление покидает цикл for и try
-блок, и выполняется обработчик исключений типа popOnEmpty
Когда возбуждается исключение,
пропускаются все инструкции, следу- ющие за той, где оно было возбуждено. Исполнение программы возобнов- ляется в catch
-обработчике этого исключения. Если такого обработчика не существует, то управление передается в функцию terminate()
, определен- ную в стандартной библиотеке C++.
Try- блок может содержать любую ин- струкцию языка C++: как выражения, так и объявления. Он вводит локаль- ную область видимости, так что объявленные внутри него переменные не- доступны вне этого блока, в том числе и в catch
-обработчиках. Например, функцию main()
можно переписать так, что объявление переменной stack окажется в try
-блоке. В таком случае обращаться к этой переменной в catch- обработчиках нельзя: int main() { try { iStack stack( 32 ); // правильно: объявление внутри try-блока stack.display(); for ( int ix = 1; ix < 51; ++ix )
{
// то же, что и раньше
}
} catch ( pushOnFull ) {
// здесь к переменной stack обращаться нельзя
} catch ( popOnEmpty ) {
// здесь к переменной stack обращаться нельзя
}
// и здесь к переменной stack обращаться нельзя return 0;
}
Можно объявить функцию так, что все ее тело будет заключено в try
-блок. При этом не обязательно помещать try
-блок внутрь определения
81 функции, удобнее заключить ее тело в функциональный try
-блок. Такая ор- ганизация поддерживает наиболее чистое разделение кода для нормальной обработки и кода для обработки исключений. Например: int main() { try { iStack stack( 32 ); // правильно: объявление внутри try-блока stack.display(); for ( int ix = 1; ix < 51; ++ix )
{
// то же, что и раньше
} return 0;
} catch ( pushOnFull ) {
// здесь к переменной stack обращаться нельзя
} catch ( popOnEmpty ) {
// здесь к переменной stack обращаться нельзя
}
}