Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
235
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

812

воспользоваться классом 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 его можно определить следующим образом:

С++ для начинающих

813

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:

С++ для начинающих

814

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

Определение шаблона класса это лишь предписание для построения бесконечного множества типов классов. Сам по себе шаблон не определяет никакого класса. Например, когда компилятор видит:

С++ для начинающих

815

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>;

программы, где этот шаблон используется:

}

Шаблон класса можно конкретизировать одним и тем же типом в нескольких файлах. Как и в случае с типами классов, когда определение класса должно присутствовать в каждом файле, где используются его члены, компилятор конкретизирует шаблон некоторым типом во всех файлах, в которых данный экземпляр употребляется в контексте, требующем полного определения класса. Чтобы определение шаблона было доступно везде, где может понадобиться конкретизация, его следует поместить в заголовочный файл.

Функции-члены и статические данные-члены шаблонов классов, а также вложенные в них типы ведут себя почти так же, как сами шаблоны. Определения членов шаблона используются для порождения экземпляров членов в конкретизированном шаблоне. Если компилятор видит:

С++ для начинающих

816

 

 

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. Модель компиляции с включением

В этой модели мы включаем определения функций-членов и статических членов шаблонов классов в каждый файл, где они конкретизируются. Для встроенных функций- членов, определенных в теле шаблона, это происходит автоматически. В противном

С++ для начинающих

817

случае такое определение следует поместить в один заголовочный файл с определением шаблона класса. Именно этой моделью мы и пользуемся в настоящей книге. Например, определения шаблонов 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() { ... }

Программа, в которой используется конкретизированная функция-член, должна перед конкретизацией включить заголовочный файл:

С++ для начинающих

818

// ---- 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():

С++ для начинающих

819

// ---- 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++.

С++ для начинающих

820

В нашей книге используется только модель с включением, так как примеры работы с шаблонами небольшие и хотелось, чтобы они компилировались максимально большим числом компиляторов.

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), необходимо применить опцию компилятора, подавляющую неявные конкретизации. Эта опция

С++ для начинающих

821

вынуждает компилятор предполагать, что все конкретизации шаблонов будут объявляться явно.

Упражнение 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;

со специализациями.)