Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdfС++ для начинающих |
802 |
Так мы определили оператор вывода для класса, конкретизированного из шаблона 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<<().
С++ для начинающих |
803 |
Распечатка элементов 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:
С++ для начинающих |
804 |
#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\n";
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 выглядит так:
С++ для начинающих |
805 |
||
|
|
#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. |
|
||
Вот шаблон для определения статических данных-членов: |
|
С++ для начинающих |
806 |
/* для каждой конкретизации 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.
С++ для начинающих |
807 |
Альтернативный подход к реализации состоит в том, чтобы вложить определение шаблона класса 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):
С++ для начинающих |
808 |
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; // правильно
Это правило применимо и тогда, когда во вложенном типе не используются параметры включающего шаблона:
С++ для начинающих |
809 |
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, за которым идет список параметров:
С++ для начинающих |
810 |
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() объявлен открытым членом и, значит, доступен во всей программе.
С++ для начинающих |
811 |
Шаблон-член конкретизируется при его использовании в программе. Например,
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. Преобразования типов допустимы. Например, если