16. шаблоны классовВ этой главе описывается, как определять и использовать шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов либо значений параметризованы. Начинающий программист может использовать шаблоны, не понимая механизма, стоящего за их определениями и конкретизациями. Фактически на протяжении всей этой книги мы пользовались шаблонами классов, которые определены в стандартной библиотеке C++ (например, vector, list и т.д.), и при этом не нуждались в детальном объяснении механизма их работы. Только профессиональные программисты определяют собственные шаблоны классов и пользуются описанными в данной главе средствами. Поэтому этот материал следует рассматривать как введение в более сложные аспекты C++. Глава 16 содержит вводные и продвинутые разделы. Во вводных разделах показано, как определяются шаблоны классов, иллюстрируются простые способы применения и обсуждается механизм их конкретизации. Мы расскажем, как можно задавать в шаблонах разные виды членов: функции-члены, статические данные- члены и вложенные типы. В продвинутых разделах представлен материал, необходимый для написания приложений промышленного уровня. Сначала мы рассмотрим, как компилятор конкретизирует шаблоны и какие требования в связи с этим предъявляются к организации нашей программы. Затем покажем, как определять специализации и частичные специализации для шаблона класса и для его члена. Далее мы остановимся на двух вопросах, представляющих интерес для проектировщиков: как разрешаются имена в определениях шаблона класса и как можно определять шаблоны в пространствах имен. Завершается эта глава примером определения и использования шаблона класса. 16.1. Определение шаблона класса Предположим, что нам нужно определить класс, поддерживающий механизм очереди. Очередь – это структура данных для хранения коллекции объектов; они помещаются в конец очереди, а извлекаются из ее начала. Поведение очереди описывают аббревиатурой FIFO – “первым пришел, первым ушел”. (Определенный в стандартной библиотеке C++ тип, реализующий очередь, упоминался в разделе 6.17. В этой главе мы создадим упрощенный тип для знакомства с шаблонами классов.) Необходимо, чтобы наш класс Queue поддерживал следующие операции: добавить элемент в конец очереди: void add( item ); удалить элемент из начала очереди: item remove(); определить, пуста ли очередь:
bool is_empty(); определить, заполнена ли очередь: bool is_full(); class Queue { public: Queue(); ~Queue(); Type& remove(); void add( const Type & ); bool is_empty(); bool is_full(); private: // ... Определение Queue могло бы выглядеть так: }; Вопрос в том, какой тип использовать вместо 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 представлена в следующих разделах с целью иллюстрации определения и применения шаблонов классов. В реализации используются две абстракции шаблона: сам шаблон класса Queue предоставляет описанный выше открытый интерфейс и пару членов: front и back. Очередь реализуется с помощью связанного списка; шаблон класса QueueItem представляет один узел связанного списка Queue. Каждый помещаемый в очередь элемент сохраняется в объекте QueueItem, который содержит два члена: value и next. Тип value будет различным в каждом экземпляре класса Queue, а next – это всегда указатель на следующий объект QueueItem в очереди. Прежде чем приступать к детальному изучению реализации этих шаблонов, рассмотрим, template <class T> как они объявляются и определяются. Вот объявление шаблона класса QueueItem: 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 могут быть подставлены различные встроенные или определенные пользователем типы. Такой процесс подстановки называется конкретизацией шаблона. Имя параметра шаблона можно употреблять после его объявления и до конца объявления или определения шаблона. Если в глобальной области видимости объявлена переменная с таким же именем, как у параметра шаблона, то это имя будет скрыто. В следующем typedef double Type; template <class Type> class QueueItem { public: // ... private: // тип Item - не double Type item; QueueItem *next; примере тип item равен не double, а типу параметра: }; 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 не указан template <class Type, size = 1024> размер буфера, то по умолчанию принимается 1024: class Buffer; В последующих объявлениях шаблона могут быть заданы дополнительные аргументы по умолчанию. Как и в объявлениях функций, если для некоторого параметра задан такой аргумент, то он должен быть задан и для всех параметров, расположенных в списке template <class Type, size = 1024> правее (даже в другом объявлении того же шаблона): class Buffer;
// правильно: рассматриваются аргументы по умолчанию из обоих объявлений template <class Type=string, int size> class Buffer; (Отметим, что аргументы по умолчанию для параметров шаблонов не поддерживаются в компиляторах, реализованных до принятия стандарта C++. Чтобы примеры из этой книги, в частности из главы 12, компилировались большинством современных компиляторов, мы не использовали такие аргументы.) Внутри определения шаблона его имя можно применять как спецификатор типа всюду, где допустимо употребление имени обычного класса. Вот более полная версия template <class Type> class QueueItem { public: QueueItem( const Type & ); private: Type item; QueueItem *next; определения шаблона QueueItem: }; Обратите внимание, что каждое появление имени QueueItem в определении шаблона – это сокращенная запись для QueueItem<Type> Такую сокращенную нотацию можно употреблять только внутри определения QueueItem (и, как мы покажем в следующих разделах, в определениях его членов, которые находятся вне определения шаблона класса). Если QueueItem применяется как спецификатор типа в определении какого-либо другого шаблона, то необходимо задавать полный список параметров. В следующем примере шаблон класса используется в определении шаблона функции display. Здесь за именем шаблона класса QueueItem должны идти параметры, template <class Type> void display( QueueItem<Type> &qi ) { QueueItem<Type> *pqi = &qi; // ... т.е. QueueItem<Type>. } 16.1.1. Определения шаблонов классов Queue и QueueItem Ниже представлено определение шаблона класса Queue. Оно помещено в заголовочный файл Queue.h вместе с определением шаблона QueueItem:
#ifndef QUEUE_H #define QUEUE_H // объявление QueueItem template <class T> class QueueItem; template <class Type> class Queue { public: Queue() : front( 0 ), back ( 0 ) { } ~Queue(); Type& remove(); void add( const Type & ); bool is_empty() const { return front == 0; } private: QueueItem<Type> *front; QueueItem<Type> *back; }; #endif При использовании имени Queue внутри определения шаблона класса Queue список параметров <Type> можно опускать. Однако пропуск списка параметров шаблона QueueItem в определении шаблона Queue недопустим. Так, объявление члена front template <class Type> class Queue { public: // ... private: // ошибка: список параметров для QueueItem неизвестен QueueItem<Type> *front; является ошибкой: } Упражнение 16.1 (a) template <class Type> class Container1; template <class Type, int size> Найдите ошибочные объявления (или пары объявлений) шаблонов классов: (b) template <class T, U, class V> class Container1; class Container2;
(c) template <class C1, typename C2> (d) template <typename myT, class myT> (e) template <class Type, int *pi> class Container3 {}; class Container4 {}; (f) template <class Type, int val = 0> class Container6; template <class T = complex<double>, int v> class Container5; class Container6; Упражнение 16.2 template <class elemenType> class ListItem; template <class elemType> class List { public: List<elemType>() : _at_front( 0 ), _at_end( 0 ), _current( 0 ), _size( 0 ) {} List<elemType>( const List<elemType> & ); List<elemType>& operator=( const List<elemType> & ); ~List(); void insert( ListItem *ptr, elemType value ); int remove( elemType value ); ListItem *find( elemType value ); void display( ostream &os = cout ); int size() { return _size; } private: ListItem *_at_front; ListItem *_at_end; ListItem *_current; int _size Следующее определение шаблона List некорректно. Как исправить ошибку?
16.2. Конкретизация шаблона класса В определении шаблона указывается, как следует строить индивидуальные классы, если заданы один или более фактических типов или значений. По шаблону Queue автоматически генерируются экземпляры классов Queue с разными типами элементов. Например, если написать: Queue<int> qi; то из обобщенного определения шаблона автоматически создается класс Queue для объектов типа int. Генерация конкретного класса из обобщенного определения шаблона называется конкретизацией шаблона. При такой конкретизации Queue для объектов типа int каждое вхождение параметра Type в определении шаблона заменяется на int, так что template <class int> class Queue { public: Queue() : front( 0 ), back ( 0 ) { } ~Queue(); int& remove(); void add( const int & ); bool is_empty() const { return front == 0; } private: QueueItem<int> *front; QueueItem<int> *back; определение класса Queue принимает вид: }; Чтобы создать класс Queue для объектов типа string, надо написать: Queue<string> qs; При этом каждое вхождение Type в определении шаблона будет заменено на string. Объекты qi и qs являются объектами автоматически созданных классов. Каждый конкретизированный по одному и тому же шаблону экземпляр класса совершенно не зависит от всех остальных. Так, у Queue для типа int нет никаких прав доступа к неоткрытым членам того же класса для типа string. Конкретизированный экземпляр шаблона будет иметь соответственно имя Queue<int> или Queue<string>. Части <int> и <string>, следующие за именем Queue, называются фактическими аргументами шаблона. Они должны быть заключены в угловые скобки и отделяться друг от друга запятыми. В имени конкретизируемого шаблона аргументы всегда должны задаваться явно. В отличие от аргументов шаблона функции, аргументы шаблона класса никогда не выводятся из контекста:
Queue qs; // ошибка: как конкретизируется шаблон? Конкретизированный шаблон класса Queue можно использовать в программе всюду, где // типы возвращаемого значения и обоих параметров конкретизированы из // шаблона класса Queue extern Queue< complex<double> > foo( Queue< complex<double> > &, Queue< complex<double> > & ); // указатель на функцию-член класса, конкретизированного из шаблона Queue bool (Queue<double>::*pmf)() = 0; // явное приведение 0 к указателю на экземпляр Queue допустимо употребление типа обычного класса: Queue<char*> *pqc = static_cast< Queue<char*>* > ( 0 ); Объекты типа класса, конкретизированного по шаблону Queue, объявляются и extern Queue<double> eqd; Queue<int> *pqi = new Queue<int>; Queue<int> aqi[1024]; int main() { int ix; if ( ! pqi->is_empty() ) ix = pqi->remove(); // ... for ( ix = 0; ix < 1024; ++ix ) eqd[ ix ].add( ix ); // ... используются так же, как объекты обычных классов: } В объявлении и определении шаблона можно ссылаться как на сам шаблон, так и на // объявление шаблона функции template <class Type> void bar( Queue<Type> &, // ссылается на обобщенный шаблон Queue<double> & // ссылается на конкретизированный шаблон конкретизированный по нему класс: ) Однако вне такого определения употребляются только конкретизированные экземпляры. Например, в теле обычной функции всегда надо задавать фактические аргументы void foo( Queue<int> &qi ) { Queue<int> *pq = &qi; // ... шаблона Queue:
} Шаблон класса конкретизируется только тогда, когда имя полученного экземпляра употребляется в контексте, где требуется определение шаблона. Не всегда определение класса должно быть известно. Например, перед объявлением указателей и ссылок на class Matrix; Matrix *pm; // правильно: определение класса Matrix знать необязательно класс его знать необязательно: void inverse( Matrix & ); // тоже правильно Поэтому объявление указателей и ссылок на конкретизированный шаблон класса не приводит к его конкретизации. (Отметим, что в некоторых компиляторах, написанных до принятия стандарта C++, шаблон конкретизируется при первом упоминании имени конкретизированного класса в тексте программы.) Так, в функции foo() объявляются // Queue<int> не конкретизируется при таком использовании в foo() void foo( Queue<int> &qi ) { Queue<int> *pqi = &qi; // ... указатель и ссылка на Queue<int>, но это не вызывает конкретизации шаблона Queue: } Определение класса необходимо знать, когда определяется объект этого типа. В следующем примере определение obj1 ошибочно: чтобы выделить для него память, class Matrix; Matrix obj1; // ошибка: класс Matrix не определен class Matrix { ... }; компилятору необходимо знать размер класса Matrix: Matrix obj2; // правильно Таким образом, конкретизация происходит тогда, когда определяется объект класса, конкретизированного по этому шаблону. В следующем примере определение объекта qi приводит к конкретизации шаблона Queue<int>: Queue<int> qi; // конкретизируется Queue<int> Определение Queue<int> становится известно компилятору именно в этой точке, которая называется точкой конкретизации данного класса. Если имеется указатель или ссылка на конкретизированный шаблон, то конкретизация также производится в момент обращения к объекту, на который они ссылаются. В определенной выше функции foo() класс Queue<int> конкретизируется в следующих случаях: когда разыменовывается указатель pqi, когда ссылка qi используется для
получения значения именуемого объекта и когда pqi или qi употребляются для доступа к void foo( Queue<int> &qi ) { Queue<int> *pqi = &qi; // Queue<int> конкретизируется в результате вызова функции-члена pqi->add( 255 ); // ... членам или функциям-членам этого класса: } Определение Queue<int> становится известным компилятору еще до вызова функции- члена add() из foo(). Напомним, что в определении шаблона класса Queue есть также ссылка на шаблон template <class Type> class Queue { public: // ... private: QueueItem<Type> *front; QueueItem<Type> *back; QueueItem: }; При конкретизации Queue типом int члены front и back становятся указателями на QueueItem<int>. Следовательно, конкретизированный экземпляр Queue<int> ссылается на экземпляр QueueItem, конкретизированный типом int. Но поскольку соответствующие члены являются указателями, то QueueItem<int> конкретизируется лишь в момент их разыменования в функциях-членах класса Queue<int>. Наш класс QueueItem служит вспомогательным средством для реализации класса Queue и не будет непосредственно употребляться в вызывающей программе. Поэтому пользовательская программа способна манипулировать только объектами Queue. Конкретизация шаблона QueueItem происходит лишь в момент конкретизации шаблона класса Queue или его членов. (В следующих разделах мы рассмотрим конкретизации членов шаблона класса.) В зависимости от типов, которыми может конкретизироваться шаблон, при его определении надо учитывать некоторые нюансы. Почему, например, следующее определение конструктора класса QueueItem не подходит для конкретизации общего template <class Type> class QueueItem { public: QueueItem( Type ); // неудачное проектное решение // ... вида? };
В данном определении аргумент передается по значению. Это допустимо, если QueueItem конкретизируется встроенным типом (например, QueueItem<int>). Но если такая конкретизация производится для объемного типа (скажем, Matrix), то накладные расходы, вызванные неправильным выбором на этапе проектирования, становятся неприемлемыми. (В разделе 7.3 обсуждались вопросы производительности, связанные с передачей параметров по значению и по ссылке.) Поэтому аргумент конструктора объявляется как ссылка на константный тип: QueueItem( const Type & ); Следующее определение приемлемо, если у типа, для которого конкретизируется template <class Type> class QueueItem { // ... public: // потенциально неэффективно QueueItem( const Type &t ) { item = t; next = 0; } QueueItem, нет ассоциированного конструктора: }; Если аргументом шаблона является тип класса с конструктором (например, string), то item инициализируется дважды! Конструктор по умолчанию string вызывается для инициализации item перед выполнением тела конструктора QueueItem. Затем для созданного объекта item производится почленное присваивание. Избежать такого можно с помощью явной инициализации item в списке инициализации членов внутри template <class Type> class QueueItem { // ... public: // item инициализируется в списке инициализации членов конструктора QueueItem( const Type &t ) : item(t) { next = 0; } определения конструктора QueueItem: }; (Списки инициализации членов и основания для их применения обсуждались в разделе 14.5.) 16.2.1. Аргументы шаблона для параметров-констант Параметр шаблона класса может и не быть типом. На аргументы, подставляемые вместо таких параметров, накладываются некоторые ограничения. В следующем примере мы изменяем определение класса Screen (см. главу 13) на шаблон, параметризованный высотой и шириной:
template <int hi, int wid> class Screen { public: Screen() : _height( hi ), _width( wid ), _cursor ( 0 ), _screen( hi * wid, '#' ) { } // ... private: string _screen; string::size_type _cursor; short _height; short _width; }; typedef Screen<24,80> termScreen; termScreen hp2621; Screen<8,24> ancientScreen; Выражение, с которым связан параметр, не являющийся типом, должно быть константным, т.е. вычисляемым во время компиляции. В примере выше typedef termScreen ссылается на экземпляр шаблона Screen<24,80>, где аргумент шаблона для hi равен 24, а для wid – 80. В обоих случаях аргумент – это константное выражение. Однако для шаблона BufPtr конкретизация приводит к ошибке, так как значение указателя, получающееся при вызове оператора new(), становится известно только во template <int *ptr> class BufPtr { ... }; // ошибка: аргумент шаблона нельзя вычислить во время компиляции время выполнения: BufPtr< new int[24] > bp; Не является константным выражением и значение неконстантного объекта. Его нельзя использовать в качестве аргумента для параметра-константы шаблона. Однако адрес любого объекта в области видимости пространства имен, в отличие от адреса локального объекта, является константным выражением (даже если спецификатор const отсутствует), поэтому его можно применять в качестве аргумента для параметра- template <int size> Buf { ... }; template <int *ptr> class BufPtr { ... }; int size_val = 1024; const int c_size_val = 1024; Buf< 1024 > buf0; // правильно Buf< c_size_val > buf1; // правильно Buf< sizeof(size_val) > buf2; // правильно: sizeof(int) BufPtr< &size_val > bp0; // правильно // ошибка: нельзя вычислить во время компиляции константы. Константным выражением будет и значение оператора sizeof: Buf< size_val > buf3;
Вот еще один пример, иллюстрирующий использование параметра-константы для представления константного значения в определении шаблона, а также применение его template < class Type, int size > class FixedArray { public: FixedArray( Type *ar ) : count( size ) { for ( int ix = 0; ix < size; ++ix ) array[ ix ] = ar[ ix ]; } private: Type array[ size ]; int count; }; int ia[4] = { 0, 1, 2, 3 }; аргумента для задания значения этого параметра: FixedArray< int, sizeof( is ) / sizeof( int ) > iA{ ia ); Выражения с одинаковыми значениями считаются эквивалентными аргументами для параметров-констант шаблона. Так, все три экземпляра Screen ссылаются на один и тот const int width = 24; const int height = 80; // все это Screen< 24, 80 > Screen< 2*12, 40*2 > scr0; Screen< 6+6+6+6, 20*2 + 40 > scr1; же конкретизированный из шаблона класс Screen<24,80>: Screen< width, height > scr2; Между типом аргумента шаблона и типом параметра-константы допустимы некоторые преобразования. Их множество является подмножеством преобразований, допустимых для аргументов функции: трансформации l-значений, включающие преобразование l-значения в r- template <int *ptr> class BufPtr { ... }; int array[10]; значение, массива в указатель и функции в указатель: BufPtr< array > bpObj; // преобразование массива в указатель template <const int *ptr> class Ptr { ... }; int iObj; преобразования квалификаторов:
Ptr< &iObj > pObj; // преобразование из int* в const int* template <int hi, int wid> class Screen { ... }; const short shi = 40; const short swi = 132; расширения типов: Screen< shi, swi > bpObj2; // расширения типа short до int template <unsigned int size> Buf{ ... }; преобразования целых типов: Buf< 1024 > bpObj; // преобразование из int в unsigned int (Более подробно они описаны в разделе 9.3.) extern void foo( char * ); extern void bar( void * ); typedef void (*PFV)( void * ); const unsigned int x = 1024; template <class Type, unsigned int size, PFV handler> class Array { ... }; Array<int, 1024U, bar> a0; // правильно: преобразование не нужно Array<int, 1024U, foo> a1; // ошибка: foo != PFV Array<int, 1024, bar> a2; // правильно: 1024 преобразуется в unsigned int Array<int, 1024, bar> a3; // ошибка: foo != PFV Array<int, x, bar> a4; // правильно: преобразование не нужно Рассмотрим следующие объявления: Array<int, x, foo> a5; // ошибка: foo != PFV Объекты a0 и a4 класса Array определены правильно, так как аргументы шаблона точно соответствуют типам параметров. Объект a2 также определен правильно, потому что аргумент 1024 типа int приводится к типу unsigned int параметра-константы size с помощью преобразования целых типов. Объявления a1, a3 и a5 ошибочны, так как не существует преобразования между любыми двумя типами функций. Приведение значения 0 целого типа к типу указателя недопустимо:
template <int *ptr> class BufPtr { ... }; // ошибка: 0 имеет тип int // неявное преобразование в нулевой указатель не применяется BufPtr< 0 > nil; Упражнение 16.3 Укажите, какие из данных конкретизированных шаблонов действительно приводят к template < class Type > class Stack { }; void f1( Stack< char > ); // (a) class Exercise { // ... Stack< double > &rsd; // (b) Stack< int > si; // (c) }; int main() { Stack< char > *sc; // (d) f1( *sc ); // (e) int iObj = sizeof( Stack< string > ); // (f) конкретизации: } Упражнение 16.4 template < int *ptr > class Ptr ( ... }; template < class Type, int size > class Fixed_Array { ... }; Какие из следующих конкретизаций шаблонов корректны? Почему? (a) const int size = 1024; template < int hi, int wid > class Screen { ... }; (b) int arr[10]; Ptr< arr > bp2; Ptr< &size > bp1; (c) Ptr < 0 > bp3;
(d) const int hi = 40; const int wi = 80; (e) const int size_val = 1024; (f) unsigned int fasize = 255; Screen< hi, wi+32 > sObj; Fixed_Array< string, size_val > fa1; (g) const double db = 3.1415; Fixed_Array< int, fasize > fa2; Fixed_Array< double, db > fa3; 16.3. Функции-члены шаблонов классов Как и для обычных классов, функция-член шаблона класса может быть определена либо внутри определения шаблона (и тогда называется встроенной), либо вне его. Мы уже встречались со встроенными функциями-членами при рассмотрении шаблона Queue. Например, конструктор Queue является встроенным, так как определен внутри template <class Type> class Queue { // ... public: // встроенный конструктор Queue() : front( 0 ), back( 0 ) { } // ... определения шаблона класса: }; При определении функции-члена шаблона вне определения самого шаблона следует применять специальный синтаксис для обозначения того, членом какого именно шаблона является функция. Определению функции-члена должно предшествовать ключевое слово template, за которым следуют параметры шаблона. Так, конструктор Queue можно определить следующим образом:
template <class Type> class Queue { public: Queue(); private: // ... }; template <class Type> inline Queue<Type>:: Queue( ) { front = back = 0; } За первым вхождением Queue (перед оператором ::) следует список параметров, показывающий, какому шаблону принадлежит данная функция-член. Второе вхождение Queue в определение конструктора (после оператора ::) содержит имя функции-члена, за которым может следовать список параметров шаблона, хотя это и необязательно. После имени функции идет ее определение;. в нем могут быть ссылки на параметр шаблона Type всюду, где в определении обычной функции использовалось бы имя типа. Функция-член шаблона класса сама является шаблоном. Стандарт C++ требует, чтобы она конкретизировалась только при вызове либо при взятии ее адреса. (Некоторые более старые компиляторы конкретизируют такие функции одновременно с конкретизацией самого шаблона класса.) При конкретизации функции-члена используется тип того объекта, для которого функция вызвана: Queue<string> qs; Объект qs имеет тип Queue<string>. При инициализации объекта этого класса вызывается конструктор Queue<string>. В данном случае аргументом, которым конкретизируется функция-член (конструктор), будет string. Функция-член шаблона конкретизируется только при реальном использовании в программе (т.е. при вызове или взятии ее адреса). От того, в какой именно момент конкретизируется функция-член, зависит разрешение имен в ее определении (см. раздел 16.11) и объявление ее специализации (см. раздел 16.9). 16.3.1. Функции-члены шаблонов Queue и QueueItem Чтобы понять, как определяются и используются функции-члены шаблонов классов, продолжим изучение шаблонов Queue и QueueItem:
template <class Type> class Queue { public: Queue() : front( 0 ), back ( 0 ) { } ~Queue(); Type& remove(); void add( const Type & ); bool is_empty() const { return front == 0; } private: QueueItem<Type> *front; QueueItem<Type> *back; }; Деструктор, а также функции-члены remove() и add() определены не в теле шаблона, а template <class Type> Queue<Type>::~Queue() { while (! is_empty() ) remove(); вне его. Деструктор Queue опустошает очередь: } template <class Type> void Queue<Type>::add( const Type &val ) { // создать новый объект QueueItem QueueItem<Type> *pt = new QueueItem<Type>( val ); if ( is_empty() ) front = back = pt; else { back->next = pt; back = pt; } Функция-член Queue<Type>::add() помещает новый элемент в конец очереди: } Функция-член Queue<Type>::remove() возвращает значение элемента, находящегося в начале очереди, и удаляет сам элемент.
#include <iostream> #include <cstdlib> template <class Type> Type Queue<Type>::remove() { if ( is_empty() ) { cerr << "remove() вызвана для пустой очереди "; exit( -1 ); } QueueItem<Type> *pt = front; front = front->next; Type retval = pt->item; delete pt; return retval; } Мы поместили определения функций-членов в заголовочный файл Queue.h, включив его в каждый файл, где возможны конкретизации функций. (Обоснование этого решения, а также рассмотрение более общих вопросов, касающихся модели компиляции шаблонов, мы отложим до раздела 16.8.) В следующей программе иллюстрируется использование и конкретизация функции-члена #include <iostream> #include "Queue.h" int main() { // конкретизируется класс Queue<int> // оператор new требует, чтобы Queue<int> был определен Queue<int> *p_qi = new Queue<int>; int ival; for ( ival = 0; ival < 10; ++ival ) // конкретизируется функция-член add() p_qi->add( ival ); int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) { // конкретизируется функция-член remove() int qval = p_qi->remove(); if ( ival != qval ) err_cnt++; } if ( !err_cnt ) cout << "!! queue executed ok "; else cerr << "?? queue errors: " << err_cnt << endl; return 0; шаблона Queue: } После компиляции и запуска программа выводит следующую строку:
!! queue executed ok Упражнение 16.5 Используя шаблон класса Screen, определенный в разделе 16.2, реализуйте функции- члены Screen (см. разделы 13.3, 13.4 и 13.6) в виде функций-членов шаблона. 16.4. Объявления друзей в шаблонах классов
обычный (не шаблонный) дружественный класс или дружественная функция. В следующем примере функция foo(), функция-член bar() и класс foobar class Foo { void bar(); }; template <class T> class QueueItem { friend class foobar; friend void foo(); friend void Foo::bar(); // ... объявлены друзьями всех конкретизаций шаблона QueueItem: }; Ни класс foobar, ни функцию foo() не обязательно объявлять или определять в глобальной области видимости перед объявлением их друзьями шаблона QueueItem. Однако перед тем как объявить другом какой-либо из членов класса Foo, необходимо определить его. Напомним, что член класса может быть введен в область видимости только через определение объемлющего класса. QueueItem не может ссылаться на Foo::bar(), пока не будет найдено определение Foo; связанный дружественный шаблон класса или функции. В следующем примере определено взаимно однозначное соответствие между классами, конкретизированными по шаблону QueueItem, и их друзьями – также конкретизациями шаблонов. Для каждого класса, конкретизированного по шаблону QueueItem, ассоциированные конкретизации foobar, foo() и template <class Type> class foobar { ... }; template <class Type> void foo( QueueItem<Type> ); template <class Type> class Queue { void bar(); // ... }; template <class Type> class QueueItem { friend class foobar<Type>; friend void foo<Type>( QueueItem<Type> ); friend void Queue<Type>::bar(); // ... Queue::bar() являются друзьями. };
Прежде чем шаблон класса можно будет использовать в объявлениях друзей, он сам должен быть объявлен или определен. В нашем примере шаблоны классов foobar и Queue, а также шаблон функции foo() следует объявить до того, как они объявлены друзьями в QueueItem. Синтаксис, использованный для объявления foo() другом, может показаться странным: friend void foo<Type>( QueueItem<Type> ); За именем функции следует список явных аргументов шаблона: foo<Type>. Такой синтаксис показывает, что в качестве друга объявляется конкретизированный шаблон функции foo(). Если бы список явных аргументов был опущен: friend void foo( QueueItem<Type> ); то компилятор интерпретировал бы объявление как относящееся к обычной функции (а не к шаблону), у которой тип параметра – это экземпляр шаблона QueueItem. Как отмечалось в разделе 10.6, шаблон функции и одноименная обычная функция могут сосуществовать, и присутствие объявления такого шаблона перед определением класса QueueItem не вынуждает компилятор соотнести объявление друга именно с ним. Для того, чтобы соотнесение было верным, в конкретизированном шаблоне функции необходимо указать список явных аргументов; несвязанный дружественный шаблон класса или функции. В следующем примере имеется отображение один-ко-многим между конкретизациями шаблона класса QueueItem и его друзьями. Для каждой конкретизации типа QueueItem все template <class Type> class QueueItem { template <class T> friend class foobar; template <class T> friend void foo( QueueItem<T> ); template <class T> friend class Queue<T>::bar(); // ... конкретизации foobar, foo() и Queue<T>::bar() являются друзьями: }; Следует отметить, что этот вид объявлений друзей в шаблоне класса не поддерживается компиляторами, написанными до принятия стандарта C++. 16.4.1. Объявления друзей в шаблонах Queue и QueueItem Поскольку QueueItem не предназначен для непосредственного использования в вызывающей программе, то объявление конструктора этого класса помещено в закрытую
секцию шаблона. Теперь класс Queue необходимо объявить другом QueueItem, чтобы можно было создавать и манипулировать объектами последнего. Существует два способа объявить шаблон класса другом. Первый заключается в том, template <class Type> class QueueItem { // любой экземпляр Queue является другом // любого экземпляра QueueItem template <class T> friend class Queue; чтобы объявить любой экземпляр Queue другом любого экземпляра QueueItem: }; Однако нет смысла объявлять, например, класс Queue, конкретизированный типом string, другом QueueItem, конкретизированного типом complex<double>. Queue<string> должен быть другом только для класса QueueItem<string>. Таким образом, нам нужно взаимно однозначное соответствие между экземплярами Queue и QueueItem, конкретизированными одинаковыми типами. Чтобы добиться этого, template <class Type> class QueueItem { // для любого экземпляра QueueItem другом является // только конкретизированный тем же типом экземпляр Queue friend class Queue<Type>; // ... применим второй метод объявления друзей: }; Данное объявление говорит о том, что для любой конкретизации QueueItem некоторым типом экземпляр Queue, конкретизированный тем же типом, является другом. Так, экземпляр Queue, конкретизированный типом int, будет другом экземпляра QueueItem, тоже конкретизированного типом int. Но для экземпляров QueueItem, конкретизированных типами complex<double> или string, этот экземпляр Queue другом не будет. В любой точке программы у пользователю может понадобиться распечатать содержимое объекта Queue. Такая возможность предоставляется с помощью перегруженного оператора вывода. Этот оператор должен быть объявлен другом шаблона Queue, так как // как задать аргумент типа Queue? ему необходим доступ к закрытым членам класса. Какой же будет его сигнатура? ostream& operator<<( ostream &, ??? ); Поскольку Queue – это шаблон класса, то в имени конкретизированного экземпляра должен быть задан полный список аргументов: ostream& operator<<( ostream &, const Queue<int> & );
Так мы определили оператор вывода для класса, конкретизированного из шаблона Queue типом int. Но что, если Queue – это очередь элементов типа string? ostream& operator<<( ostream &, const Queue<string> & ); Вместо того чтобы явно определять нужный оператор вывода по мере необходимости, желательно сразу определить общий оператор, который будет работать для любой конкретизации Queue. Например: ostream& operator<<( ostream &, const Queue<Type> & ); template <class Type> ostream& Однако из этого перегруженного оператора вывода придется сделать шаблон функции: operator<<( ostream &, const Queue<Type> & ); Теперь всякий раз, когда оператору ostream передается конкретизированный экземпляр Queue, конкретизируется и вызывается шаблон функции. Вот одна из возможных template <class Type> ostream& operator<<( ostream &os, const Queue<Type> &q ) { os << "< "; QueueItem<Type> *p; for ( p = q.front; p; p = p->next ) os << *p << " "; os << " >"; return os; реализаций оператора вывода в виде такого шаблона: } Если очередь объектов типа int содержит значения 3, 5, 8, 13, то распечатка ее содержимого с помощью такого оператора дает < 3 5 8 13 > Обратите внимание, что оператор вывода обращается к закрытому члену front класса template <class Type> class Queue { friend ostream& operator<<( ostream &, const Queue<Type> & ); // ... Queue. Поэтому оператор необходимо объявить другом Queue: }; Здесь, как и при объявлении друга в шаблоне класса Queue, создается взаимно однозначное соответствие между конкретизациями Queue и оператора operator<<().
Распечатка элементов Queue производится оператором вывода operator<<() класса QueueItem: os << *p; Этот оператор также должен быть реализован в виде шаблона функции; тогда можно template <class Type> ostream& operator<<( ostream &os, const QueueItem<Type> &qi ) { os << qi.item; return os; быть уверенным, что в нужный момент будет конкретизирован подходящий экземпляр: } Поскольку здесь имеется обращение к закрытому члену item класса QueueItem, оператор template <class Type> class QueueItem { friend class Queue<Type>; friend ostream& operator<<( ostream &, const QueueItem<Type> & ); // ... следует объявить другом шаблона QueueItem. Это делается следующим образом: }; Оператор вывода класса QueueItem полагается на то, что item умеет распечатывать себя: os << qi.item; Это порождает тонкую зависимость типов при конкретизации Queue. Любой определенный пользователем и связанный с Queue класс, содержимое которого нужно распечатывать, должен предоставлять оператор вывода. В языке нет механизма, с помощью которого можно было бы задать такую зависимость в определении самого шаблона Queue. Но если оператор вывода не определен для типа, с которым конкретизируется данный шаблон, и делается попытка вывести содержимое конкретизированного экземпляра, то в том месте, где используется отсутствующий оператор вывода, компилятор выдает сообщение об ошибке. Шаблон Queue можно конкретизировать типом, не имеющим оператора вывода, – при условии, что не будет попытки распечатать содержимое очереди. Следующая программа демонстрирует конкретизацию и использование функций-друзей шаблонов классов Queue и QueueItem:
#include <iostream> #include "Queue.h" int main() { Queue<int> qi; // конкретизируются оба экземпляра // ostream& operator<<(ostream &os, const Queue<int> &) // ostream& operator<<(ostream &os, const QueueItem<int> &) cout << qi << endl; int ival; for ( ival = 0; ival < 10; ++ival ) qi.add( ival ); cout << qi << endl; int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) { int qval = qi.remove(); if ( ival != qval ) err_cnt++; } cout << qi << endl; if ( !err_cnt ) cout << "!! queue executed ok "; else cout << "?? queue errors: " << err_cnt << endl; return 0; } После компиляции и запуска программа выдает результат: < > < 0 1 2 3 4 5 6 7 8 9 > < > !! queue executed ok Упражнение 16.6 Пользуясь шаблоном класса Screen, определенным в упражнении 16.5, реализуйте операторы ввода и вывода (см. упражнение 15.6 из раздела 15.2) в виде шаблонов. Объясните, почему вы выбрали тот, а не иной способ объявления друзей класса Screen, добавленных в его шаблон. 16.5. Статические члены шаблонов класса В шаблоне класса могут быть объявлены статические данные-члены. Каждый конкретизированный экземпляр имеет собственный набор таких членов. Рассмотрим операторы new() и delete() для шаблона QueueItem. В класс QueueItem нужно static QueueItem<Type> *free_list; добавить два статических члена: static const unsigned QueueItem_chunk; Модифицированное определение шаблона QueueItem выглядит так:
#include <cstddef> template <class Type> class QueueItem { // ... private: void *operator new( size_t ); void operator delete( void *, size_t ); // ... static QueueItem *free_list; static const unsigned QueueItem_chunk; // ... }; Операторы new() и delete() объявлены закрытыми, чтобы предотвратить создание объектов типа QueueItem вызывающей программой: это разрешается только членам и друзьям QueueItem (к примеру, шаблону Queue). template <class Type> void* QueueItem<Type>::operator new( size_t size ) { QueueItem<Type> *p; if ( ! free_list ) { size_t chunk = QueueItem_chunk * size; free_list = p = reinterpret_cast< QueueItem<Type>* > ( new char[chunk] ); for ( ; p != &free_list[ QueueItem_chunk - 1 ]; ++p ) p->next = p + 1; p->next = 0; } p = free_list; free_list = free_list->next; return p; Оператор new() можно реализовать таким образом: } template <class Type> void QueueItem<Type>:: operator delete( void *p, size_t ) { static_cast< QueueItem<Type>* >( p )->next = free_list; free_list = static_cast< QueueItem<Type>* > ( p ); А реализация оператора delete() выглядит так: } Теперь остается инициализировать статические члены free_list и QueueItem_chunk. Вот шаблон для определения статических данных-членов:
/* для каждой конкретизации QueueItem сгенерировать * соответствующий free_list и инициализировать его нулем */ template <class T> QueueItem<T> *QueueItem<T>::free_list = 0; /* для каждой конкретизации QueueItem сгенерировать * соответствующий QueueItem_chunk и инициализировать его значением 24 */ template <class T> const unsigned int QueueItem<T>::QueueItem_chunk = 24; Определение шаблона статического члена должно быть вынесено за пределы определения самого шаблона класса, которое начинается с ключевого слово template с последующим списком параметров <class T>. Имени статического члена предшествует префикс QueueItem<T>::, показывающий, что этот член принадлежит именно шаблону QueueItem. Определения таких членов помещаются в заголовочный файл Queue.h и должны включаться во все файлы, где производится их конкретизация. (В разделе 16.8 мы объясним, почему решили делать именно так, и затронем другие вопросы, касающиеся модели компиляции шаблонов.) Статический член конкретизируется по шаблону только в том случае, когда реально используется в программе. Сам такой член тоже является шаблоном. Определение шаблона для него не приводит к выделению памяти: она выделяется только для конкретизированного экземпляра статического члена. Каждая подобная конкретизация соответствует конкретизации шаблона класса. Таким образом, обращение к экземпляру статического члена всегда производится через некоторый конкретизированный экземпляр // ошибка: QueueItem - это не реальный конкретизированный экземпляр int ival0 = QueueItem::QueueItem_chunk; int ival1 = QueueItem<string>::QueueItem_chunk; // правильно класса: int ival2 = QueueItem<int>::QueueItem_chunk; // правильно Упражнение 16.7 Реализуйте определенные в разделе 15.8 операторы new() и delete() и относящиеся к ним статические члены screenChunk и freeStore для шаблона класса Screen, построенного в упражнении 16.6. 16.6. Вложенные типы шаблонов классов Шаблон класса QueueItem применяется только как вспомогательное средство для реализации Queue. Чтобы запретить любое другое использование, в шаблоне QueueItem имеется закрытый конструктор, позволяющий создавать объекты этого класса исключительно функциям-членам класса Queue, объявленным друзьями QueueItem. Хотя шаблон QueueItem виден во всей программе, создать объекты этого класса или обратиться к его членам можно только при посредстве функций-членов Queue.
Альтернативный подход к реализации состоит в том, чтобы вложить определение шаблона класса QueueItem в закрытую секцию шаблона Queue. Поскольку QueueItem является вложенным закрытым типом, он становится недоступным вызывающей программе, и обратиться к нему можно лишь из шаблона класса Queue и его друзей (например, оператора вывода). Если же сделать члены QueueItem открытыми, то объявлять Queue другом QueueItem не понадобится. Семантика исходной реализации при этом сохраняется, но отношение между шаблонами QueueItem и Queue моделируется более элегантно. Поскольку при любой конкретизации шаблона Queue требуется конкретизировать тем же типом и QueueItem, то вложенный класс должен быть шаблоном. Вложенные классы шаблонов сами являются шаблонами классов, а параметры объемлющего шаблона можно template <class Type> class Queue: // ... private: class QueueItem { public: QueueItem( Type val ) : item( val ), next( 0 ) { ... } Type item; QueueItem *next; }; // поскольку QueueItem - вложенный тип, // а не шаблон, определенный вне Queue, // то аргумент шаблона <Type> после QueueItem можно опустить QueueItem *front, *back; // ... использовать во вложенном: }; При каждой конкретизации Queue создается также класс QueueItem с подходящим аргументом для Type. Между конкретизациями шаблонов QueueItem и Queue имеется взаимно однозначное соответствие. Вложенный в шаблон класс конкретизируется только в том случае, если он используется в контексте, где требуется полный тип класса. В разделе 16.2 мы упоминали, что конкретизация шаблона класса Queue типом int не означает автоматической конкретизации и класса QueueItem<int>. Члены front и back – это указатели на QueueItem<int>, а если объявлены только указатели на некоторый тип, то конкретизировать соответствующий класс не обязательно, хотя QueueItem вложен в шаблон класса Queue. QueueItem<int> конкретизируется только тогда, когда указатели front или back разыменовываются в функциях-членах класса Queue<int>. Внутри шаблона класса можно также объявлять перечисления и определять типы (с помощью typedef):
template <class Type, int size> class Buffer: public: enum Buf_vals { last = size-1, Buf_size }; typedef Type BufType; BufType array[ size ]; // ... } Вместо того чтобы явно включать член Buf_size, в шаблоне класса Buffer объявляется перечисление с двумя элементами, которые инициализируются значением параметра шаблона. Например, объявление Buffer<int, 512> small_buf; устанавливает Buf_size в 512, а last – в 511. Аналогично Buffer<int, 1024> medium_buf; устанавливает Buf_size в 1024, а last – в 1023. Открытый вложенный тип разрешается использовать и вне определения объемлющего класса. Однако вызывающая программа может ссылаться лишь на конкретизированные экземпляры подобного типа (или элементов вложенного перечисления). В таком случае имени вложенного типа должно предшествовать имя конкретизированного шаблона // ошибка: какая конкретизация Buffer? Buffer::Buf_vals bfv0; класса: Buffer<int,512>::Buf_vals bfv1; // правильно Это правило применимо и тогда, когда во вложенном типе не используются параметры включающего шаблона:
template <class T> class Q { public: enum QA { empty, full }; // не зависит от параметров QA status; // ... }; #include <iostream> int main() { Q<double> qd; Q<int> qi; qd.status = Q::empty; // ошибка: какая конкретизация Q? qd.status = Q<double>::empty; // правильно int val1 = Q<double>::empty; int val2 = Q<int>::empty; if ( val1 != val2 ) cerr << "ошибка реализации!" << endl; return 0; } Во всех конкретизациях Q значения empty одинаковы, но при ссылке на empty необходимо указывать, какому именно экземпляру Q принадлежит перечисление. Упражнение 16.8 Определите класс List и вложенный в него ListItem из раздела 13.10 как шаблоны. Реализуйте аналогичные определения для ассоциированных членов класса. 16.7. Шаблоны-члены Шаблон функции или класса может быть членом обычного класса или шаблона класса. Определение шаблона-члена похоже на определение шаблона: ему предшествует ключевое слово template, за которым идет список параметров:
template <class T> class Queue { private: // шаблон класса-члена template <class Type> class CL { Type member; T mem; }; // ... public: // шаблон функции-члена template <class Iter> void assign( Iter first, Iter last ) { while ( ! is_empty() ) remove(); // вызывается Queue<T>::remove() for ( ; first != last; ++first ) add( *first ); // вызывается Queue<T>::add( const T & ) } } (Отметим, что шаблоны-члены не поддерживаются компиляторами, написанными до принятия стандарта C++. Эта возможность была добавлена в язык для поддержки реализации абстрактных контейнерных типов, представленных в главе 6.) Объявление шаблона-члена имеет собственные параметры. Например, у шаблона класса CL есть параметр Type, а у шаблона функции assign() – параметр Iter. Помимо этого, в определении шаблона-члена могут использоваться параметры объемлющего шаблона класса. Например, у шаблона CL есть член типа T, представляющего параметр включающего шаблона Queue. Объявление шаблона-члена в шаблоне класса Queue означает, что конкретизация Queue потенциально может содержать бесконечное число различных вложенных классов CL функций-членов assign(). Так, конкретизированный экземпляр Queue<int> включает Queue<int>::CL<char> вложенные типы: Queue<int>::CL<string> void Queue<int>::assign( int *, int * ) void Queue<int>::assign( vector<int>::iterator,
и вложенные функции:
vector<int>::iterator )
Для шаблона-члена действуют те же правила доступа, что и для других членов класса. Так как шаблон CL является закрытым членом шаблона Queue, то лишь функции-члены и друзья Queue могут ссылаться на его конкретизации. С другой стороны, шаблон функции assign() объявлен открытым членом и, значит, доступен во всей программе.
Шаблон-член конкретизируется при его использовании в программе. Например, int main() { // конкретизация Queue<int> Queue<int> qi; // конкретизация Queue<int>::assign( int *, int * ) int ai[4] = { 0, 3, 6, 9 }; qi.assign( ai, ai + 4 ); // конкретизация Queue<int>::assign( vector<int>::iterator, // vector<int>::iterator ) vector<int> vi( ai, ai + 4 ); qi.assign( vi.begin(), vi.end() ); assign() конкретизируется в момент обращения к ней из main(): } Шаблон функции assign(), являющийся членом шаблона класса Queue, иллюстрирует необходимость применения шаблонов-членов для поддержки контейнерных типов. Предположим, имеется очередь типа Queue<int>, в которую нужно поместить содержимое любого другого контейнера (списка, вектора или обычного массива), причем его элементы имеют либо тип int (т.е. тот же, что у элементов очереди), либо приводимый к типу int. Шаблон-член assign()позволяет это сделать. Поскольку может быть использован любой контейнерный тип, то интерфейс assign() программируется в расчете на употребление итераторов; в результате реализация оказывается не зависящей от фактического типа, на который итераторы указывают. В функции main() шаблон-член assign() сначала конкретизируется типом int*, что позволяет поместить в qi содержимое массива элементов типа int. Затем шаблон-член конкретизируется типом vector<int>::iterator – это дает возможность поместить в очередь qi содержимое вектора элементов типа int. Контейнер, содержимое которого помещается в очередь, не обязательно должен состоять из элементов типа int. Разрешен любой тип, который приводится к int. Чтобы понять, почему это так, еще раз посмотрим template <class Iter> void assign( Iter first, Iter last ) { // удалить все элементы из очереди for ( ; first != last; ++first ) add( *first ); на определение assign(): } Вызываемая из assign() функция add() – это функция-член Queue<Type>::add(). Если Queue конкретизируется типом int, то у add() будет следующий прототип: void Queue<int>::add( const int &val ); Аргумент *first должен иметь тип int либо тип, которым можно инициализировать параметр-ссылку на const int. Преобразования типов допустимы. Например, если
воспользоваться классом SmallInt из раздела 15.9, то содержимое контейнера, в котором хранятся элементы типа SmallInt, с помощью шаблона-члена assign() помещается в очередь типа Queue<int>. Это возможно потому, что в классе SmallInt имеется class SmallInt { public: SmallInt( int ival = 0 ) : value( ival ) { } // конвертер: SmallInt ==> int operator int() { return value; } // ... private: int value; }; int main() { // конкретизация Queue<int> Queue<int> qi; vector<SmallInt> vsi; // заполнить вектор // конкретизация // Queue<int>::assign( vector<SmallInt>::iterator, // vector<SmallInt>::iterator ) qi.assign( vsi.begin(), vsi.end() ); list<int*> lpi; // заполнить список // ошибка при конкретизации шаблона-члена assign(): // нет преобразования из int* в int qi.assign( lpi.begin(), lpi.end() ); конвертер для приведения SmallInt к int: } Первая конкретизация assign() правильна, так как существует неявное преобразование из типа SmallInt в тип int и, следовательно, обращение к add() корректно. Вторая же конкретизация ошибочна: объект типа int* не может инициализировать ссылку на тип const int, поэтому вызвать функцию add() невозможно. Для контейнерных типов из стандартной библиотеки C++ имеется функция assign(), которая ведет себя так же, как функция-шаблон assign() для нашего класса Queue. Любую функцию-член можно задать в виде шаблона. Это относится, в частности, к конструктору. Например, для шаблона класса Queue его можно определить следующим образом:
template <class T> class Queue { // ... public: // шаблон-член конструктора template <class Iter> Queue( Iter first, Iter last ) : front( 0 ), back( 0 ) { for ( ; first != last; ++first ) add( * first ); } }; Такой конструктор позволяет инициализировать очередь содержимым другого контейнера. У контейнерных типов из стандартной библиотеки C++ также есть предназначенные для этой цели конструкторы в виде шаблонов-членов. Кстати, в первом (в данном разделе) определении функции main() использовался конструктор-шаблон для вектора: vector<int> vi( ai, ai + 4 ); Это определение конкретизирует шаблон конструктора для контейнера vector<int> типом int*, что позволяет инициализировать вектор содержимым массива элементов типа int. Шаблон-член, как и обычные члены, может быть определен вне определения объемлющего класса или шаблона класса. Так, являющиеся членами шаблон класса CL или шаблон функции assign() могут быть следующим образом определены вне шаблона Queue:
template <class T> class Queue { private: template <class Type> class CL; // ... public: template <class Iter> void assign( Iter first, Iter last ); // ... }; template <class T> template <class Type> class Queue<T>::CL<Type> { Type member; T mem; }; template <class T> template <class Iter> void Queue<T>::assign( Iter first, Iter last ) { while ( ! is_empty() ) remove(); for ( ; first != last; ++first ) add( *first ); } Определению шаблона-члена, которое находится вне определения объемлющего шаблона класса, предшествует список параметров объемлющего шаблона класса, а за ним должен следовать собственный такой список. Вот почему определение шаблона функции assign() (члена шаблона класса Queue) начинается с template <class T> template <class Iter> Первый список параметров шаблона template <class T> относится к шаблону класса Queue. Второй – к самому шаблону-члену assign(). Имена параметров не обязаны совпадать с теми, которые указаны внутри определения объемлющего шаблона класса. Приведенная инструкция по-прежнему определяет шаблон-член assign(): void Queue<TT>::assign( IterType first, IterType last ) template <class TT> template <class IterType> { ... } 16.8. Шаблоны классов и модель компиляции A Определение шаблона класса – это лишь предписание для построения бесконечного множества типов классов. Сам по себе шаблон не определяет никакого класса. Например, когда компилятор видит:
template <class Type> class Queue { ... }; он только сохраняет внутреннее представление Queue. Позже, когда встречается реальное int main() { Queue<int> *p_qi = new Queue<int>; использование класса, конкретизированного по шаблону, скажем: } компилятор конкретизирует тип класса Queue<int>, применяя сохраненное внутреннее представление определения шаблона Queue. Шаблон конкретизируется только тогда, когда он употребляется в контексте, требующем полного определения класса. (Этот вопрос подробно обсуждался в разделе 16.2.) В примере выше класс Queue<int> конкретизируется, потому что компилятор должен знать размер типа Queue<int>, чтобы выделить нужный объем памяти для объекта, созданного оператором new. Компилятор может конкретизировать шаблон только тогда, когда он видел не только его объявление, но и фактическое определение, которое должно предшествовать тому месту // объявление шаблона класса template <class Type> class Queue; Queue<int>* global_pi = 0; // правильно: определение класса не нужно int main() { // ошибка: необходима конкретизация // определение шаблона класса должно быть видимо Queue<int> *p_qi = new Queue<int>; программы, где этот шаблон используется: } Шаблон класса можно конкретизировать одним и тем же типом в нескольких файлах. Как и в случае с типами классов, когда определение класса должно присутствовать в каждом файле, где используются его члены, компилятор конкретизирует шаблон некоторым типом во всех файлах, в которых данный экземпляр употребляется в контексте, требующем полного определения класса. Чтобы определение шаблона было доступно везде, где может понадобиться конкретизация, его следует поместить в заголовочный файл. Функции-члены и статические данные-члены шаблонов классов, а также вложенные в них типы ведут себя почти так же, как сами шаблоны. Определения членов шаблона используются для порождения экземпляров членов в конкретизированном шаблоне. Если компилятор видит:
template <class Type> void Queue<Type>::add( const Type &val ) { ... } он сохраняет внутреннее представление Queue<Type>::add(). Позже, когда в программе встречается фактическое употребление этой функции-члена, допустим через объект типа Queue<int>, компилятор конкретизирует Queue<int>::add(const int &), пользуясь #include "Queue.h" int main() { // конкретизация Queue<int> Queue<int> *p_qi = new Queue<int>; int ival; // ... // конкретизация Queue<int>::add( const int & ) p_qi->add( ival ); // ... таким представлением: } Конкретизация шаблона класса некоторым типом не приводит к автоматической конкретизации всех его членов тем же типом. Член конкретизируется только при использовании в таком контексте, где необходимо его определение (т.е. вложенный тип употреблен так, что требуется его полное определение; вызвана функция-член или взят ее адрес; имеется обращение к значению статического члена). Конкретизация функций-членов и статических членов шаблонов класса поднимает те же вопросы, которые мы уже обсуждали для шаблонов функций в разделе 10.5. Чтобы компилятор мог конкретизировать функцию-член или статический член шаблона класса, должно ли определение члена быть видимым в момент конкретизации? Например, должно ли определение функции-члена add() появиться до ее конкретизации типом int в main()? Следует ли помещать определения функций-членов и статических членов шаблонов класса в заголовочные файлы (как мы поступаем с определениями встроенных функций), которые включаются всюду, где применяются их конкретизированные экземпляры? Или конкретизации определения шаблона достаточно для того, чтобы этими членами можно было пользоваться, так что определения членов можно оставлять в файлах с исходными текстами (где обычно располагаются определения невстроенных функций-членов и статических членов)? Для ответа на эти вопросы нам придется вспомнить модель компиляции шаблонов в C++, где формулируются требования к организации программы, в которой определяются и употребляются шаблоны. Обе модели (с включением и с разделением), описанные в разделе 10.5, в полной мере применимы и к определениям функций-членов и статических данных-членов шаблонов классов. В оставшейся части этого раздела описываются обе модели и объясняется их использование с определениями членов. 16.8.1. Модель компиляции с включением В этой модели мы включаем определения функций-членов и статических членов шаблонов классов в каждый файл, где они конкретизируются. Для встроенных функций- членов, определенных в теле шаблона, это происходит автоматически. В противном
случае такое определение следует поместить в один заголовочный файл с определением шаблона класса. Именно этой моделью мы и пользуемся в настоящей книге. Например, определения шаблонов Queue и QueueItem, как и их функций-членов и статических членов, находятся в заголовочном файле Queue.h. Подобное размещение не лишено недостатков: определения функций-членов могут быть довольно большими и содержать детали реализации, которые неинтересны пользователям или должны быть скрыты от них. Кроме того, многократная компиляция одного определения шаблона при обработке разных файлов увеличивает общее время компиляции программы. Описанная модель (если она доступна) позволяет отделить интерфейс шаблона от реализации (т.е. от определений функций-членов и статических данных-членов). 16.8.2. Модель компиляции с разделением В этой модели определение шаблона класса и определения встроенных функций-членов помещаются в заголовочный файл, а определения невстроенных функций-членов и статических данных-членов – в файл с исходным текстом программы. Иными словами, определения шаблона класса и его членов организованы так же, как определения // ---- Queue.h ---- // объявляет Queue как экспортируемый шаблон класса export template <class Type> class Queue { // ... public: Type& remove(); void add( const Type & ); // ... обычных классов (не шаблонов) и их членов: // ---- Queue.C ---- // экспортированное определение шаблона класса Queue // находится в Queue.h #include "Queue.h" template <class Type> void Queue<Type>::add( const Type &val ) { ... } template <class Type> }; Type& Queue<Type>::remove() { ... } Программа, в которой используется конкретизированная функция-член, должна перед конкретизацией включить заголовочный файл:
// ---- User.C ---- #include "Queue.h" int main() { // конкретизация Queue<int> Queue<int> *p_qi = new Queue<int>; int ival; // ... // правильно: конкретизация Queue<int>::add( const int & ) p_qi->add( ival ); // ... } Хотя определение шаблона для функции-члена add() не видно в файле User.C, конкретизированный экземпляр Queue<int>::add(const int &) вызывать оттуда можно. Но для этого шаблон класса необходимо объявить экспортируемым. Если он экспортируется, то для использования конкретизированных функций-членов или статических данных-членов необходимо знать лишь определение самого шаблона. Определения членов могут отсутствовать в тех файлах, где они конкретизируются. Чтобы объявить шаблон класса экспортируемым, перед словом template в его export template <class Type> определении или объявлении нужно поставить ключевое слово export: class Queue { ... }; В нашем примере слово export применено к шаблону класса Queue в файле Queue.h; этот файл включен в файл Queue.C, содержащий определения функций-членов add() и remove(), которые автоматически становятся экспортируемыми и не должны присутствовать в других файлах перед конкретизацией. Отметим, что, хотя шаблон класса объявлен экспортируемым, его собственное определение должно присутствовать в файле User.C. Конкретизация Queue<int>::add() в User.C вводит определение класса, в котором объявлены функции-члены Queue<int>::add() и Queue<int>::remove(). Эти объявления обязаны предшествовать вызову указанных функций. Таким образом, слово export влияет лишь на обработку функций-членов и статических данных-членов. Экспортируемыми можно объявлять также отдельные члены шаблона. В этом случае ключевое слово export указывается не перед шаблоном класса, а только перед экспортируемыми членами. Например, если автор шаблона класса Queue хочет экспортировать лишь функцию-член Queue<Type>::add() (т.е. изъять из заголовочного файла Queue.h только ее определение), то слово export можно указать именно в определении функции-члена add():
// ---- Queue.h ---- template <class Type> class Queue { // ... public: Type& remove(); void add( const Type & ); // ... }; // необходимо, так как remove() не экспортируется template <class Type> // ---- Queue.C ---- #include "Queue.h" // экспортируется только функция-член add() export template <class Type> Type& Queue<Type>::remove() { ... } void Queue<Type>::add( const Type &val ) { ... } Обратите внимание, что определение шаблона для функции-члена remove() перенесено в заголовочный файл Queue.h. Это необходимо, поскольку remove() более не находится в экспортируемом шаблоне и, следовательно, ее определение должно быть видно во всех файлах, где вызываются конкретизированные экземпляры. Определение функции-члена или статического члена шаблона объявляется экспортируемым только один раз во всей программе. Поскольку компилятор обрабатывает файлы последовательно, он обычно не в состоянии определить, что эти члены объявлены экспортируемыми в нескольких исходных файлах. В таком случае результаты могут быть следующими: при редактировании связей возникает ошибка, показывающая, что один и тот же член шаблона класса определен несколько раз; компилятор неоднократно конкретизирует некоторый член одним и тем же множеством аргументов шаблона, что приводит к ошибке повторного определения во время связывания программы; компилятор конкретизирует член с помощью одного из экспортированных определений шаблона, игнорируя все остальные. Следовательно, нельзя утверждать, что при наличии в программе нескольких определений экспортированного члена шаблона обязательно будет сгенерирована ошибка. Создавая программу, надо быть внимательным и следить за тем, чтобы определения членов находились только в одном исходном файле. Модель с разделением позволяет отделить интерфейс шаблона класса от его реализации и организовать программу так, что эти интерфейсы помещаются в заголовочные файлы, а реализации – в файлы с исходным текстом. Однако не все компиляторы поддерживают данную модель, а те, которые поддерживают, не всегда делают это правильно: для этого требуется более изощренная среда программирования, которая доступна не во всех реализациях C++.
В нашей книге используется только модель с включением, так как примеры работы с шаблонами небольшие и хотелось, чтобы они компилировались максимально большим числом компиляторов. 16.8.3. Явные объявления конкретизации При использовании модели с включением определение члена шаблона класса помещается в каждый исходный файл, где может употребляться конкретизированный экземпляр. Точно неизвестно, где и когда компилятор конкретизирует такое определение, и некоторые компиляторы (особенно более старые) конкретизируют определение члена данным множеством аргументов шаблона неоднократно. Для использования в программе (на этапе сборки или на одной из предшествующих ей стадий) выбирается один из полученных экземпляров, а остальные игнорируются. Результат работы программы не зависит от того, сколько раз конкретизировался шаблон: в конечном итоге употребляется лишь один экземпляр. Однако, если приложение состоит из большого числа файлов и некоторый шаблон конкретизируется в каждом из них, то время компиляции заметно возрастает. Подобные проблемы, характерные для старых компиляторов, затрудняли использование шаблонов. Чтобы помочь программисту управлять моментом, когда конкретизация происходит, в стандарте C++ введено понятие явного объявления конкретизации, где за ключевым словом template идет слово class и имя конкретизируемого шаблона класса. В следующем примере явно объявляется конкретизация шаблона Queue<int>, в котором #include "Queue.h" // явное объявление конкретизации запрашивается конкретизация аргументом int шаблона класса Queue: template class Queue<int>; Если шаблон класса конкретизируется явно, то явно конкретизируются и все его члены, причем тем же типом аргумента. Следовательно, в файле, где встречается явное объявление, должно присутствовать не только определение шаблона, но и определения template <class Type> class Queue; // ошибка: шаблон Queue и его члены не определены всех его членов. В противном случае выдается сообщение об ошибке: template class Queue<int>; Если в некотором исходном файле встречается явное объявление конкретизации, то что произойдет в других файлах, где используется такой же конкретизированный шаблон? Как сказать компилятору, что явное объявление имеется в другом файле и что при употреблении шаблона класса или его членов в этом файле конкретизировать ничего не надо? Здесь, как и при использовании шаблонов функций (см. раздел 10.5.3), необходимо применить опцию компилятора, подавляющую неявные конкретизации. Эта опция
вынуждает компилятор предполагать, что все конкретизации шаблонов будут объявляться явно. Упражнение 16.9 Куда бы вы поместили определения функций-членов и статических данных-членов своих шаблонов классов, если имеющийся у вас компилятор поддерживает модель компиляции с разделением? Объясните почему. Упражнение 16.10 Имеется шаблон класса Screen, разработанный в упражнениях из предыдущих разделов (в том числе функции-члены, определенные в упражнении 16.5 из раздела 16.3, и статические члены, определенные в упражнении 16.7 из раздела 16.5). Организуйте программу так, чтобы воспользоваться преимуществами модели компиляции с разделением. 16.9. Специализации шаблонов классов A Прежде чем приступать к рассмотрению специализаций шаблонов классов и причин, по которым в них может возникнуть надобность, добавим в шаблон Queue функции-члены min() и max(). Они будут обходить все элементы очереди и искать среди них соответственно минимальное и максимальное значения (правильнее, конечно, использовать для этой цели обобщенные алгоритмы min() и max(), представленные в главе 12, но мы определим эти функции как члены шаблона Queue, чтобы познакомиться template <class Type> class Queue { // ... public: Type min(); Type max(); // ... }; // найти минимальное значение в очереди Queue template <class Type> Type Queue<Type>::min() { assert( ! is_empty() ); Type min_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item < min_val ) min_val = pq->item; return min_val; } // найти максимальное значение в очереди Queue template <class Type> Type Queue<Type>::max() { assert( ! is_empty() ); Type max_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item > max_val ) max_val = pq->item; return max_val; со специализациями.)
} Следующая инструкция в функции-члене min() сравнивает два элемента очереди Queue: pq->item < min_val Здесь неявно присутствует требование к типам, которыми может конкретизироваться шаблон класса Queue: такой тип должен либо иметь возможность пользоваться предопределенным оператором “меньше” для встроенных типов, либо быть классом, в котором определен оператор operator<(). Если же этого оператора нет, то попытка применить min() к очереди приведет к ошибке компиляции в том месте, где вызывается несуществующий оператор сравнения. (Аналогичная проблема существует и в max(), только касается оператора operator>()). class LongSouble { public: LongDouble( double dbval ) : value( dval ) { } bool compareLess( const LongDouble & ); private: double value; Предположим, что шаблон класса Queue нужно конкретизировать таким типом: }; Но в этом классе нет оператора operator<(), позволяющего сравнивать два значения типа LongDouble, поэтому использовать для очереди типа Queue<LongDouble> функции- члены min() и max() нельзя. Одним из решений этой проблемы может стать определение глобальных operator<() и operator>(), в которых для сравнения значений типа Queue<LongDouble> используется функция-член compareLess. Эти глобальные операторы вызывались бы из min() и max() автоматически при сравнении объектов из очереди. Однако мы рассмотрим другое решение, связанное со специализацией шаблонов класса: вместо общих определений функций-членов min() и max() при конкретизации шаблона Queue типом LongDouble мы определим специальные экземпляры Queue<LongDouble>::min() и Queue<LongDouble>::max(), основанные на функции- члене compareLess() класса LongDouble. Это можно сделать, если воспользоваться явным определением специализации, где после ключевого слова template идет пара угловых скобок <>, а за ней – определение специализации члена класса. В приведенном примере для функций-членов min() и max() класса Queue<LongDouble>, конкретизированного из шаблона, определены явные специализации:
// определения явных специализаций template<> LongDouble Queue<LongDouble>::min() { assert( ! is_empty() ); LongDouble min_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item.compareLess( min_val ) ) min_val = pq->item; return min_val; } template<> LongDouble Queue<LongDouble>::max() { assert( ! is_empty() ); LongDouble max_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( max_val.compareLess( pq->item ) ) max_val = pq->item; return max_val; } Хотя тип класса Queue<LongDouble> конкретизируется по шаблону, в каждом объекте этого типа используются специализированные функции-члены min() и max() – не те, что конкретизируются по обобщенным определениям этих функций в шаблоне класса Queue. Поскольку определения явных специализаций min() и max() – это определения невстроенных функций, помещать их в заголовочный файл нельзя: они обязаны находится в файле с текстом программы. Однако явную специализацию функции можно // объявления явных специализаций функций-членов template <> LongDouble Queue<LongDouble>::min(); объявить, не определяя. Например: template <> LongDouble Queue<LongDouble>::max(); Поместив эти объявления в заголовочный файл, а соответствующие определения – в исходный, мы можем организовать код так же, как и для определений функций-членов обычного класса. Иногда определение всего шаблона оказывается непригодным для конкретизации некоторым типом. В таком случае программист может специализировать шаблон класса целиком. Напишем полное определение класса Queue<LongDouble>:
// QueueLD.h: определяет специализацию класса Queue<LongDouble> #include "Queue.h" template<> Queue<LongDouble> { Queue<LongDouble>(); ~Queue<LongDouble>(); LongDouble& remove(); void add( const LongDouble & ); bool is_empty() const; LongDouble min(); LongDouble max(); private: // Некоторая реализация }; Явную специализацию шаблона класса можно определять только после того, как общий шаблон уже был объявлен (хотя и не обязательно определен). Иными словами, должно быть известно, что специализируемое имя обозначает шаблон класса. Если в приведенном примере не включить заголовочный файл Queue.h перед определением явной специализации шаблона, компилятор выдаст сообщение об ошибке, указывая, что Queue – это не имя шаблона. Если мы определяем специализацию всего шаблона класса, то должны определить также все без исключения функции-члены и статические данные-члены. Определения членов из общего шаблона никогда не используются для создания определений членов явной специализации: множества членов этих шаблонов могут различаться. Чтобы предоставить определение явной специализации для типа класса Queue<LongDouble>, придется определить не только функции-члены min() и max(), но и все остальные. Если класс специализируется целиком, лексемы template<> помещаются только перед #include "QueueLD.h" // определяет функцию-член min() // из специализированного шаблона класса определением явной специализации всего шаблона: LongDouble Queue<LongDouble>::min() { } Класс не может в одних файлах конкретизироваться из общего определения шаблона, а в других – из специализированного, если задано одно и то же множество аргументов. Например, специализацию шаблона QueueItem<LongDouble> необходимо объявлять в // ---- File1.C ---- #include "Queue.h" void ReadIn( Queue<LongDouble> *pq ) { // использование pq->add() // приводит к конкретизации QueueItem<LongDouble> } каждом файле, где она используется:
// ---- File2.C ---- #include "QueueLD.h" void ReadIn( Queue<LongDouble> * ); int main() { // используется определение специализации для Queue<LongDouble> Queue<LongDouble> *qld = new Queue<LongDouble>; ReadIn( qld ); // ... } Эта программа некорректна, хотя большинство компиляторов ошибку не обнаружат: заголовочный файл QueueLD.h следует включать во все файлы, где используется Queue<LongDouble>, причем до первого использования. 16.10. Частичные специализации шаблонов классов A Если у шаблона класса есть несколько параметров, то можно специализировать его только для одного или нескольких аргументов, оставляя другие неспециализированными. Иными словами, допустимо написать шаблон, соответствующий общему во всем, кроме тех параметров, вместо которых подставлены фактические типы или значения. Такой механизм носит название частичной специализации шаблона класса. Она может понадобиться при определении реализации, более подходящей для конкретного набора аргументов. Рассмотрим шаблон класса Screen, введенный в разделе 16.2. Частичная специализации template <int hi, int wid> class Screen { // ... }; // частичная специализация шаблона класса Screen template <int hi> class Screen<hi, 80> { public: Screen(); // ... private: string _screen; string::size_type _cursor; short _height; // для экранов с 80 колонками используются специальные алгоритмы Screen<hi,80> дает более эффективную реализацию для экранов с 80 столбцами: }; Частичная специализация шаблона класса – это шаблон, и ее определение похоже на определение шаблона. Оно начинается с ключевого слова template, за которым следует список параметров, заключенный в угловые скобки. Список параметров здесь отличается от соответствующего списка параметров общего шаблона. Для частичной специализации шаблона Screen есть только один параметр-константа hi, поскольку значение второго
аргумента равно 80, т.е. в данном списке представлены только те параметры, для которых фактические аргументы еще неизвестны. Имя частичной специализации совпадает с именем того общего шаблона, которому она соответствует, в нашем случае Screen. Однако за ее именем всегда следует список аргументов. В примере выше этот список выглядит как <hi,80>. Поскольку значение аргумента для первого параметра шаблона неизвестно, то на этом месте в списке стоит имя параметра шаблона; вторым же аргументом является значение 80, которым частично специализирован шаблон. Частичная специализация шаблона класса неявно конкретизируется при использовании в программе. В следующем примере частичная специализация конкретизируется аргументом шаблона 24 вместо hi: Screen<24,80> hp2621; Обратите внимание, что экземпляр Screen<24,80> может быть конкретизирован не только из частично специализированного, но и из общего шаблона. Почему же тогда компилятор остановился именно на частичной специализации? Если для шаблона класса объявлены частичные специализации, компилятор выбирает то определение, которое является наиболее специализированным для заданных аргументов. Если же ни одно из них не подходит, используется общее определение шаблона. Например, при конкретизации экземпляра Screen<40,132> соответствующей аргументам шаблона специализации нет. Наш вариант применяется только для конкретизации типа Screen с 80 колонками. Определение частичной специализации не связано с определением общего шаблона. У него может быть совершенно другой набор членов, а также собственные определения функций-членов, статических членов и вложенных типов. Содержащиеся в общем шаблоне определения членов никогда не употребляются для конкретизации членов его частичной специализации. Например, для частичной специализации Screen<hi,80> // конструктор для частичной специализации Screen<hi,80> template <int hi> Screen<hi,80>::Screen() : _height( hi ), _cursor( 0 ), _screen( hi * 80, bk ) должен быть определен свой конструктор: { } Если для конкретизации некоторого класса применяется частичная специализация, то определение конструктора из общего шаблона не используется даже тогда, когда определение конструктора Screen<hi,80> отсутствует. 16.11. Разрешение имен в шаблонах классов A При обсуждении разрешения имен в шаблонах функций (см. раздел 10.9) мы уже говорили о том, что этот процесс выполняется в два шага. Так же разрешаются имена и в определениях шаблонов классов и их членов. Каждый шаг относится к разным видам имен: первый – к тем, которые имеют один и тот же смысл во всех экземплярах шаблона, а второй – к тем, которые потенциально могут иметь разный смысл в разных экземплярах. Рассмотрим несколько примеров, где используется функция-член remove() шаблона класса Queue:
// Queue.h: #include <iostream> #include <cstdlib> // определение класса Queue template <class Type> Type Queue<Type>::remove() { if ( is_empty() ) { cerr << "remove() вызвана для пустой очереди "; exit(-1); } QueueItem<Type> *pt = front; front = front->next; Type retval = pt->item; delete pt; cout << "удалено значение: "; cout << retval << endl; return retval; } В выражении cout << retval << endl; переменная retval имеет тип Type, и ее фактический тип неизвестен до конкретизации функции-члена remove(). То, какой оператор operator<<() будет выбран, зависит от фактического типа retval, подставленного вместо Type. При разных конкретизациях remove() могут вызываться разные operator<<(). Поэтому мы говорим, что выбранный оператор вывода зависит от параметра шаблона. Однако для вызова функции exit() ситуация иная. Ее фактическим аргументом является литерал, значение которого одинаково при всех конкретизациях remove(). Поскольку при обращении к функции не используются аргументы, типы которых зависят от параметра шаблона Type, гарантируется, что всегда будет вызываться exit(), объявленная в заголовочном файле cstdlib. По той же причине в выражении cout << "удалено значение: "; всегда вызывается глобальный оператор ostream& operator<<( ostream &, const char * ); Аргумент "удалено значение: " – это C-строка символов, и ее тип не зависит от параметра шаблона Type. Поэтому в любом конкретизированном экземпляре remove()употребление operator<<() имеет одинаковый смысл. Один и тот же смысл во всех конкретизациях шаблона имеют те конструкции, которые не зависят от параметров шаблона. Таким образом, два шага разрешения имени в определениях шаблонов классов или их членов состоят в следующем: Имена, не зависящие от параметров шаблона, разрешаются во время его определения.
Имена, зависящие от параметров шаблона, разрешаются во время его конкретизации. Такой подход удовлетворяет требованиям как разработчика класса, так и его пользователя. Например, разработчикам необходимо управлять процессом разрешения имен. Если шаблон класса входит в состав библиотеки, в которой определены также другие шаблоны и функции, то желательно, чтобы при конкретизации шаблона класса и его членов по возможности применялись именно библиотечные компоненты. Это гарантирует первый шаг разрешения имени. Если использованное в определении шаблона имя не зависит от параметров шаблона, то оно разрешается в результате просмотра всех объявлений, видимых в заголовочном файле, включенном перед определением шаблона. Разработчик класса должен позаботиться о том, чтобы были видимы объявления всех не зависящих от параметров шаблона имен, употребленных в его определении. Если объявление такого имени не найдено, то определение шаблона считается ошибочным. Если бы перед определением функции-члена remove() в шаблоне класса Queue не были включены файлы iostream и cstdlib, то в выражении cout << "удалено значение: "; и при компиляции вызова функции exit() были бы обнаружены ошибки. Второй шаг разрешения имени необходим, если поиск производится среди функций и операторов, зависящих от типа, которым конкретизирован шаблон. Например, если шаблон класса Queue конкретизируется типом класса LongDouble (см. раздел 16.9), то желательно, чтобы внутри функции-члена remove()в следующем выражении cout << retval << endl; #include "Queue.h" #include "ldouble.h" // содержит: // class LongDouble { ... }; // ostream& operator<<( ostream &, const LongDouble & ); int main() { // конкретизация Queue<LongDouble> Queue<LongDouble> *qld = new Queue<LongDouble>; // конкретизация Queue<LongDouble>::remove() // вызывает оператор вывода для LongDouble qld->remove(); // ... вызывался оператор operator<<(), ассоциированный с классом LongDouble: } Место в программе, где происходит конкретизация шаблона, называется точкой конкретизации. Она определяет, какие объявления принимаются компилятором во внимание для имен, зависящих от параметров шаблона. Точка конкретизации шаблона всегда находится в области видимости пространства имен и непосредственно предшествует объявлению или определению, которое ссылается на
конкретизированный экземпляр. Точка конкретизации функции-члена или статического члена шаблона класса всегда следует непосредственно за объявлением или определением, которое ссылается на конкретизированный член. В предыдущем примере точка конкретизации Queue<LongDouble> находится перед main(), и при разрешении зависящих от параметров имен, которые используются в определении шаблона Queue, компилятор просматривает все объявления до этой точки. Аналогично при таком разрешении в определении remove() компилятор просматривает все объявления до точки конкретизации, расположенной после main(). Как отмечалось в разделе 16.2, шаблон конкретизируется, если он используется в контексте, требующем полного определения класса. Члены шаблона не конкретизируются автоматически вместе с ним, а лишь тогда, когда сами используются в программе. Поэтому точка конкретизации шаблона класса может не совпадать с точками конкретизации его членов, да и сами члены могут конкретизироваться в разных точках. Чтобы избежать ошибок, объявления имен, упоминаемых в определениях шаблона и его членов, рекомендуется помещать в заголовочные файлы, включая их перед первой конкретизацией шаблона класса или любого из его членов. 16.12. Пространства имен и шаблоны классов Как и любое определение в глобальной области видимости, определение шаблона класса можно поместить внутрь пространства имен. (Пространства имен рассматривались в разделах 8.5 и 8.6.) Наш шаблон будет скрыт в данном пространстве имен; лишь в этом отличие от ситуации, когда шаблон определен в глобальной области видимости. При употреблении вне пространства имя шаблона следует либо квалифицировать его именем, #include <iostream> #include <cstdlib> namespace cplusplus_primer { template <class Type> class Queue { // ... }; template <class Type> Type Queue<Type>::remove() { // ... } либо воспользоваться using-объявлением: } Если имя Queue шаблона класса используется вне пространства имен cplusplus_primer, то оно должно быть квалифицировано этим именем или введено с помощью using-объявления. Во всех остальных отношениях шаблон Queue используется так, как описано выше: конкретизируется, может иметь функции-члены, статические члены, вложенные типы и т.д. Например:
int main() { using cplusplus_primer Queue; // using-объявление // ссылается на шаблон класса в пространстве имен cplusplus_primer Queue<int> *p_qi = new Queue<int>; // ... p_qi->remove(); } Шаблон cplusplus_primer::Queue<int> конкретизируется, так как использован в выражении new: ... = new Queue<int>; p_qi – это указатель на тип класса cplusplus_primer::Queue<int>. Когда он применяется для адресации функции-члена remove(), то речь идет о члене именно этого конкретизированного экземпляра класса. Объявление шаблона класса в пространстве имен влияет также на объявления специализаций и частичных специализаций шаблона класса и его членов (см. разделы 16.9 и 16.10). Такая специализация должна быть объявлена в том же пространстве имен, где и общий шаблон. В следующем примере в пространстве имен cplusplus_primer объявляются специализации типа класса Queue<char *> и функции-члена remove() класса #include <iostream> #include <cstdlib> namespace cplusplus_primer { template <class Type> class Queue { ... }; template <class Type> Type Queue<Type>::remove() { ... } // объявление специализации // для cplusplus_primer::Queue<char *> template<> class Queue<char*> { ... }; // объявление специализации // для функции-члена cplusplus_primer::Queue<double>::remove() template<> double Queue<double>::remove() { ... } Queue<double>: } Хотя специализации являются членами cplusplus_primer, их определения в этом пространстве отсутствуют. Определить специализацию шаблона можно и вне пространства имен при условии, что определение будет находиться в некотором пространстве, объемлющем cplusplus_primer, и имя специализации будет квалифицировано его именем :
namespace cplusplus_primer { // определение Queue и его функций-членов } // объявление специализации // cplusplus_primer::Queue<char*> template<> class cplusplus_primer::Queue<char*> { ... }; // объявление специализации функции-члена // cplusplus_primer::Queue<double>::remove() template<> double cplusplus_primer::Queue<double>::remove() { ... } Объявления специализаций класса cplusplus_primer::Queue<char*> и функции-члена remove() для класса cplusplus_primer::Queue<double> находятся в глобальной области видимости. Поскольку такая область содержит пространство имен cplusplus_primer, а имена специализаций квалифицированы его именем, то определения специализаций для шаблона Queue вполне законны. 16.13. Шаблон класса Array В этом разделе мы завершим реализацию шаблона класса Array, введенного в разделе 2.5 (этот шаблон будет распространен на одиночное наследование в разделе 18.3 и на множественное наследование в разделе 18.6). Так выглядит полный заголовочный файл:
#ifndef ARRAY_H #define ARRAY_H #include <iostream> template <class elemType> class Array; template <class elemType> ostream& operator<<( ostream &, Array<elemType> & ); template <class elemType> class Array { public: explicit Array( int sz = DefaultArraySize ) { init( 0, sz ); } Array( const elemType *ar, int sz ) { init( ar, sz ); } Array( const Array &iA ) { init( iA._ia, iA._size ); } ~Array() { delete[] _ia; } Array & operator=( const Array & ); int size() const { return _size; } elemType& operator[]( int ix ) const { return _ia[ix]; } ostream &print( ostream& os = cout ) const; void grow(); void sort( int,int ); int find( elemType ); elemType min(); elemType max(); private: void init( const elemType*, int ); void swap( int, int ); static const int DefaultArraySize = 12; int _size; elemType *_ia; }; #endif Код, общий для реализации всех трех конструкторов, вынесен в отдельную функцию- член init(). Поскольку она не должна напрямую вызываться пользователями шаблона класса Array, мы поместили ее в закрытую секцию:
template <class elemType> void Array<elemType>::init( const elemType *array, int sz ) { _size = sz; _ia = new elemType[ _size ]; for ( int ix = 0; ix < _size; ++ix ) if ( ! array ) _ia[ ix ] = 0; else _ia[ ix ] = array[ ix ]; } Реализация копирующего оператора присваивания не вызывает затруднений. Как template <class elemType> Array<elemType>& Array<elemType>::operator=( const Array<elemType> &iA ) { if ( this != &iA ) { delete[] _ia; init( iA._ia, iA._size ); } return *this; отмечалось в разделе 14.7, в код включена защита от копирования объекта в самого себя: } Функция-член print() отвечает за вывод объекта того типа, которым конкретизирован шаблон Array. Возможно, реализация несколько сложнее, чем необходимо, зато данные аккуратно размещаются на странице. Если экземпляр конкретизированного класса Array<int> содержит элементы 3, 5, 8, 13 и 21, то выведены они будут так: (5) < 3, 5, 8, 13, 21 > Оператор потокового вывода просто вызывает print(). Ниже приведена реализация обеих функций:
template <class elemType> ostream& operator<<( ostream &os, Array<elemType> &ar ) { return ar.print( os ); } template <class elemType> ostream & Array<elemType>::print( ostream &os ) const { const int lineLength = 12; os << "( " << _size << " )< "; for ( int ix = 0; ix < _size; ++ix ) { if ( ix \% lineLength == 0 && ix ) os << " "; os << _ia[ ix ]; // не выводить запятую за последним элементом в строке, // а также за последним элементом массива if ( ix \% lineLength != lineLength-1 && ix != _size-1 ) os << ", "; } os << " > "; return os; } Вывод значения элемента массива в функции print() осуществляет такая инструкция: os << _ia[ ix ]; Для ее правильной работы должно выполняться требование к типам, которыми конкретизируется шаблон Array: такой тип должен быть встроенным либо иметь собственный оператор вывода. В противном случае любая попытка распечатать содержимое класса Array приведет к ошибке компиляции в том месте, где используется несуществующий оператор. Функция-член grow() увеличивает размер объекта класса Array. В нашем примере – в полтора раза:
template <class elemType> void Array<elemType>::grow() { elemType *oldia = _ia; int oldSize = _size; _size = oldSize + oldSize/2 + 1; _ia = new elemType[_size]; int ix; for ( ix = 0; ix < oldSize; ++ix ) _ia[ix] = oldia[ix]; for ( ; ix < _size; ++ix ) _ia[ix] = elemType(); delete[] oldia; } Функции-члены find(), min() и max() осуществляют последовательный поиск во внутреннем массиве _ia. Если бы массив был отсортирован, то, конечно, их можно было template <class elemType> elemType Array<elemType>::min( ) { assert( _ia != 0 ); elemType min_val = _ia[0]; for ( int ix = 1; ix < _size; ++ix ) if ( _ia[ix] < min_val ) min_val = _ia[ix]; return min_val; } template <class elemType> elemType Array<elemType>::max() { assert( _ia != 0 ); elemType max_val = _ia[0]; for ( int ix = 1; ix < _size; ++ix ) if ( max_val < _ia[ix] ) max_val = _ia[ix]; return max_val; } template <class elemType> int Array<elemType>::find( elemType val ) { for ( int ix = 0; ix < _size; ++ix ) if ( val == _ia[ix] ) return ix; return -1; бы реализовать гораздо эффективнее. }
В шаблоне класса Array есть функция-член sort(), реализованная с помощью алгоритма быстрой сортировки. Она очень похожа на шаблон функции, представленный в разделе 10.11. Функция-член swap() – вспомогательная утилита для sort(); она не template <class elemType> void Array<elemType>::swap( int i, int j ) { elemType tmp = _ia[i]; _ia[i] = _ia[j]; _ia[j] = tmp; } template <class elemType> void Array<elemType>::sort( int low, int high ) { if ( low >= high ) return; int lo = low; int hi = high + 1; elemType elem = _ia[low]; for ( ;; ) { while ( _ia[++lo] < elem ) ; while ( _ia[--hi] > elem ) ; if ( lo < hi ) swap( lo,hi ); else break; } swap( low, hi ); sort( low, hi-1 ); sort( hi+1, high ); является частью открытого интерфейса шаблона и потому помещена в закрытую секцию: } То, что код реализован, разумеется, не означает, что он работоспособен. try_array() – это шаблон функции, предназначенный для тестирования реализации шаблона Array:
#include "Array.h" template <class elemType> void try_array( Array<elemType> &iA ) { cout << "try_array: начальные значения массива "; cout << iA << endl; elemType find_val = iA [ iA.size()-1 ]; iA[ iA.size()-1 ] = iA.min(); int mid = iA.size()/2; iA[0] = iA.max(); iA[mid] = iA[0]; cout << "try_array: после присваиваний "; cout << iA << endl; Array<elemType> iA2 = iA; iA2[mid/2] = iA2[mid]; cout << "try_array: почленная инициализация "; cout << iA << endl; iA = iA2; cout << "try_array: после почленного копирования "; cout << iA << endl; iA.grow(); cout << "try_array: после вызова grow "; cout << iA << endl; int index = iA.find( find_val ); cout << "искомое значение: " << find_val; cout << " возвращенный индекс: " << index << endl; elemType value = iA[index]; cout << "значение элемента с этим индексом: "; cout << value << endl; } Рассмотрим шаблон функции try_array(). На первом шаге печатается исходный объект Array, что подтверждает успешную конкретизацию оператора вывода шаблона, а заодно дает начальную картину, с которой можно будет сверяться при последующих модификациях. В переменной find_val хранится значение, которое мы впоследствии передадим find(). Если бы try_array() была обычной функцией, роль такого значения сыграла бы константа. Но поскольку никакая константа не может обслужить все типы, которыми допустимо конкретизировать шаблон, то приходится выбирать другой путь. Далее одним элементам Array случайным образом присваиваются значения других элементов, чтобы протестировать min(), max(), size() и, конечно, оператор взятия индекса. Затем объект iA2 почленно инициализируется объектом iA, что приводит к вызову копирующего конструктора. После этого тестируется оператор взятия индекса с объектом ia2: производится присваивание элементу с индексом mid/2. (Эти две строки представляют интерес в случае, когда iA – производный подтип Array, а оператор взятия индекса объявлен виртуальной функцией. Мы вернемся к этому в главе 18 при обсуждении наследования.) Далее в iA почленно копируется модифицированный объект iA2, что приводит к вызову копирующего оператора присваивания класса Array. Затем проверяются функции-члены grow() и find(). Напомним, что find() возвращает значение –1, если искомый элемент не найден. Попытка выбрать из “массива” Array
элемент с индексом –1 приведет к выходу за левую границу. (В главе 18 для перехвата этой ошибки мы построим производный от Array класс, который будет проверять выход за границы массива.) Убедиться, что наша реализация шаблона работает для различных типов данных, например целых чисел, чисел с плавающей точкой и строк, поможет программа main(), #include "Array.C" #include "try_array.C" #include <string> int main() { static int ia[] = { 12,7,14,9,128,17,6,3,27,5 }; static double da[] = { 12.3,7.9,14.6,9.8,128.0 }; static string sa[] = { "Eeyore", "Pooh", "Tigger", "Piglet", "Owl", "Gopher", "Heffalump" }; Array<int> iA( ia, sizeof(ia)/sizeof(int) ); Array<double> dA( da, sizeof(da)/sizeof(double) ); Array<string> sA( sa, sizeof(sa)/sizeof(string) ); cout << "template Array<int> class " << endl; try_array(iA); cout << "template Array<double> class " << endl; try_array(dA); cout << "template Array<string> class " << endl; try_array(sA); return 0; которая вызывает try_array() с каждым из указанных типов: } Вот что программа выводит при конкретизации шаблона Array типом double: try_array: начальные значения массива ( 5 )< 12.3, 7.9, 14.6, 9.8, 128 > try_array: после присваиваний ( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 > try_array: почленная инициализация ( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 > try_array: после почленного копирования ( 5 )< 14.6, 14.6, 14.6, 9.8, 7.9 > try_array: после вызова grow ( 8 )< 14.6, 14.6, 14.6, 9.8, 7.9, 0, 0, 0 > искомое значение: 128 возвращенный индекс: -1 значение элемента с этим индексом: 3.35965e-322 Выход индекса за границу массива приводит к тому, что последнее напечатанное программой значение неверно. Конкретизация шаблона Array типом string заканчивается крахом программы:
template Array<string> class try_array: начальные значения массива ( 7 )< Eeyore, Pooh, Tigger, Piglet, Owl, Gopher, Heffalump > try_array: после присваиваний ( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: почленная инициализация ( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: после почленного копирования ( 7 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore > try_array: после вызова grow ( 11 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore, <пусто>, <пусто>, <пусто>, <пусто> > искомое значение: Heffalump возвращенный индекс: -1 Memory fault (coredump) Упражнение 16.11 Измените шаблон класса Array, убрав из него функции-члены sort(), find(), max(), min() и swap(), и модифицируйте шаблон try_array() так, чтобы она вместо них пользовалась обобщенными алгоритмами (см. главу 12).
Часть V
Объектно-ориентированное программирование
Объектно-ориентированное программирование расширяет объектное программирование, вводя отношения тип-подтип с помощью механизма, именуемого наследованием. Вместо того чтобы заново реализовывать общие свойства, класс наследует данные-члены и функции-члены родительского класса. В языке C++ наследование осуществляется посредством так называемого порождения производных классов. Класс, свойства которого наследуются, называется базовым, а новый класс – производным. Все множество базовых и производных классов образует иерархию наследования. Например, в трехмерной компьютерной графике классы OrthographicCamera и PerspectiveCamera обычно являются производными от базового Camera. Множество операций и данных, общее для всех камер, определено в абстрактном классе Camera. Каждый производный от него класс реализует лишь отличия от абстрактной камеры, предоставляя альтернативный код для унаследованных функций-членов либо вводя дополнительные члены. Если базовый и производный классы имеют общий открытый интерфейс, то производный называется подтипом базового. Так, PerspectiveCamera является подтипом класса Camera. В C++ существует специальное отношение между типом и подтипом, позволяющее указателю или ссылке на базовый класс адресовать любой из производных от него подтипов без вмешательства программиста. (Такая возможность манипулировать несколькими типами с помощью указателя или ссылки на базовый класс называется полиморфизмом.) Если дана функция: void lookAt( const Camera *pCamera ); то мы реализуем lookAt(), программируя интерфейс базового класса Camera и не заботясь о том, на что указывает pCamera: на объект класса PerspectiveCamera, на объект класса OrthographicCamera или на объект, описывающий еще какой-то вид камеры, который мы пока не определили. При каждом вызове lookAt() ей передается адрес объекта, принадлежащего к одному из подтипов Camera. Компилятор автоматически преобразует его в указатель на подходящий // правильно: автоматически преобразуется в Camera* OrthographicCamera ocam; lookAt( &ocam ); // ... // правильно: автоматически преобразуется в Camera* PerspectiveCamera *pcam = new PerspectiveCamera; базовый класс: lookAt( pcam ); Наша реализация lookAt() не зависит от набора подтипов класса Camera, реально существующих в приложении. Если впоследствии потребуется добавить новый подтип или исключить существующий, то изменять реализацию lookAt() не придется.
Полиморфизм подтипов позволяет написать ядро приложения так, что оно не будет зависеть от конкретных типов, которыми мы манипулируем. Мы программируем открытый интерфейс базового класса придуманной нами абстракции, пользуясь только ссылками и указателями на него. При работе программы будет определен фактический тип адресуемого объекта и вызвана подходящая реализация открытого интерфейса. Нахождение (или разрешение) нужной функции во время выполнения называется динамическим связыванием (dynamic binding) (по умолчанию функции разрешаются статически во время компиляции). В C++ динамическое связывание поддерживается с помощью механизма виртуальных функций класса. Полиморфизм подтипов и динамическое связывание формируют основу объектно-ориентированного программирования, которому посвящены следующие главы. В главе 17 рассматриваются имеющиеся в C++ средства поддержки объектно- ориентированного программирования и изучается влияние наследование на такие механизмы, как конструкторы, деструкторы, почленная инициализация и присваивание; для примера разрабатывается иерархия классов Query, поддерживающая систему текстового поиска, введенную в главе 6. Темой главы 18 является изучение более сложных иерархий, возможных за счет использования множественного и виртуального наследования. С его помощью мы развернем шаблон класса из главы 16 в трехуровневую иерархию. В главе 19 обсуждается идентификация типов во время выполнения (RTTI), а также изучается вопрос о влиянии наследования на разрешение перегруженных функций. Здесь мы снова обратимся к средствам обработки исключений, чтобы разобраться в иерархии классов исключений, которую предлагает стандартная библиотека. Мы покажем также, как написать собственные такие классы. Глава 20 посвящена углубленному рассмотрению библиотеки потокового ввода/вывода iostream. Эта библиотека представляет собой иерархию классов, поддерживающую как виртуальное, так и множественное наследование. 17 |
| Оглавление| |