<< Пред.           стр. 76 (из 121)           След. >>

Список литературы по разделу

  // для любого экземпляра QueueItem другом является
  // только конкретизированный тем же типом экземпляр Queue
  friend class Queue;
 
  // ...
 };
 Данное объявление говорит о том, что для любой конкретизации QueueItem некоторым типом экземпляр Queue, конкретизированный тем же типом, является другом. Так, экземпляр Queue, конкретизированный типом int, будет другом экземпляра QueueItem, тоже конкретизированного типом int. Но для экземпляров QueueItem, конкретизированных типами complex или string, этот экземпляр Queue другом не будет.
 В любой точке программы у пользователю может понадобиться распечатать содержимое объекта Queue. Такая возможность предоставляется с помощью перегруженного оператора вывода. Этот оператор должен быть объявлен другом шаблона Queue, так как ему необходим доступ к закрытым членам класса. Какой же будет его сигнатура?
 // как задать аргумент типа Queue?
 ostream& operator<<( ostream &, ??? );
 Поскольку Queue – это шаблон класса, то в имени конкретизированного экземпляра должен быть задан полный список аргументов:
 ostream& operator<<( ostream &, const Queue & );
 Так мы определили оператор вывода для класса, конкретизированного из шаблона Queue типом int. Но что, если Queue – это очередь элементов типа string?
 ostream& operator<<( ostream &, const Queue & );
 Вместо того чтобы явно определять нужный оператор вывода по мере необходимости, желательно сразу определить общий оператор, который будет работать для любой конкретизации Queue. Например:
 ostream& operator<<( ostream &, const Queue & );
 Однако из этого перегруженного оператора вывода придется сделать шаблон функции:
 template ostream&
  operator<<( ostream &, const Queue & );
 Теперь всякий раз, когда оператору ostream передается конкретизированный экземпляр Queue, конкретизируется и вызывается шаблон функции. Вот одна из возможных реализаций оператора вывода в виде такого шаблона:
 template
 ostream& operator<<( ostream &os, const Queue &q )
 {
  os << "< ";
  QueueItem *p;
  for ( p = q.front; p; p = p->next )
  os << *p << " ";
  os << " >";
  return os;
 }
 Если очередь объектов типа int содержит значения 3, 5, 8, 13, то распечатка ее содержимого с помощью такого оператора дает
 
 < 3 5 8 13 >
 
 Обратите внимание, что оператор вывода обращается к закрытому члену front класса Queue. Поэтому оператор необходимо объявить другом Queue:
 template
 class Queue {
  friend ostream&
  operator<<( ostream &, const Queue & );
  // ...
 };
 Здесь, как и при объявлении друга в шаблоне класса Queue, создается взаимно однозначное соответствие между конкретизациями Queue и оператора operator<<().
 Распечатка элементов Queue производится оператором вывода operator<<() класса QueueItem:
 os << *p;
 Этот оператор также должен быть реализован в виде шаблона функции; тогда можно быть уверенным, что в нужный момент будет конкретизирован подходящий экземпляр:
 template
 ostream& operator<<( ostream &os, const QueueItem &qi )
 {
  os << qi.item;
  return os;
 }
 Поскольку здесь имеется обращение к закрытому члену item класса QueueItem, оператор следует объявить другом шаблона QueueItem. Это делается следующим образом:
 template
 class QueueItem {
  friend class Queue;
  friend ostream&
  operator<<( ostream &, const QueueItem & );
  // ...
 };
 Оператор вывода класса QueueItem полагается на то, что item умеет распечатывать себя:
 os << qi.item;
 Это порождает тонкую зависимость типов при конкретизации Queue. Любой определенный пользователем и связанный с Queue класс, содержимое которого нужно распечатывать, должен предоставлять оператор вывода. В языке нет механизма, с помощью которого можно было бы задать такую зависимость в определении самого шаблона Queue. Но если оператор вывода не определен для типа, с которым конкретизируется данный шаблон, и делается попытка вывести содержимое конкретизированного экземпляра, то в том месте, где используется отсутствующий оператор вывода, компилятор выдает сообщение об ошибке. Шаблон Queue можно конкретизировать типом, не имеющим оператора вывода, – при условии, что не будет попытки распечатать содержимое очереди.
 Следующая программа демонстрирует конкретизацию и использование функций-друзей шаблонов классов Queue и QueueItem:
 #include
 #include "Queue.h"
 
 int main() {
  Queue qi;
  // конкретизируются оба экземпляра
  // ostream& operator<<(ostream &os, const Queue &)
  // ostream& operator<<(ostream &os, const QueueItem &)
  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 *free_list;
 static const unsigned QueueItem_chunk;
 Модифицированное определение шаблона QueueItem выглядит так:
 #include
 
 template
 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).
 Оператор new() можно реализовать таким образом:
 template void*
  QueueItem::operator new( size_t size )
 {
  QueueItem *p;
  if ( ! free_list )
  {
  size_t chunk = QueueItem_chunk * size;
  free_list = p =
  reinterpret_cast< QueueItem* >
  ( 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;
 }
 А реализация оператора delete() выглядит так:
 template
 void QueueItem::
  operator delete( void *p, size_t )
 {
  static_cast< QueueItem* >( p )->next = free_list;
  free_list = static_cast< QueueItem* > ( p );
 }
 Теперь остается инициализировать статические члены free_list и QueueItem_chunk. Вот шаблон для определения статических данных-членов:
 /* для каждой конкретизации QueueItem сгенерировать
  * соответствующий free_list и инициализировать его нулем
  */
 template
  QueueItem *QueueItem::free_list = 0;
 
 /* для каждой конкретизации QueueItem сгенерировать
  * соответствующий QueueItem_chunk и инициализировать его значением 24
  */
 template
  const unsigned int
  QueueItem::QueueItem_chunk = 24;
 Определение шаблона статического члена должно быть вынесено за пределы определения самого шаблона класса, которое начинается с ключевого слово template с последующим списком параметров . Имени статического члена предшествует префикс QueueItem::, показывающий, что этот член принадлежит именно шаблону QueueItem. Определения таких членов помещаются в заголовочный файл Queue.h и должны включаться во все файлы, где производится их конкретизация. (В разделе 16.8 мы объясним, почему решили делать именно так, и затронем другие вопросы, касающиеся модели компиляции шаблонов.)
 Статический член конкретизируется по шаблону только в том случае, когда реально используется в программе. Сам такой член тоже является шаблоном. Определение шаблона для него не приводит к выделению памяти: она выделяется только для конкретизированного экземпляра статического члена. Каждая подобная конкретизация соответствует конкретизации шаблона класса. Таким образом, обращение к экземпляру статического члена всегда производится через некоторый конкретизированный экземпляр класса:
 // ошибка: QueueItem - это не реальный конкретизированный экземпляр
 int ival0 = QueueItem::QueueItem_chunk;
 
 int ival1 = QueueItem::QueueItem_chunk; // правильно
 int ival2 = QueueItem::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 Queue:
  // ...
 private:
  class QueueItem {
  public:
  QueueItem( Type val )
  : item( val ), next( 0 ) { ... }
 
  Type item;
  QueueItem *next;
  };
  // поскольку QueueItem - вложенный тип,
  // а не шаблон, определенный вне Queue,
  // то аргумент шаблона после QueueItem можно опустить
  QueueItem *front, *back;
  // ...
 };
 При каждой конкретизации Queue создается также класс QueueItem с подходящим аргументом для Type. Между конкретизациями шаблонов QueueItem и Queue имеется взаимно однозначное соответствие.
 Вложенный в шаблон класс конкретизируется только в том случае, если он используется в контексте, где требуется полный тип класса. В разделе 16.2 мы упоминали, что конкретизация шаблона класса Queue типом int не означает автоматической конкретизации и класса QueueItem. Члены front и back – это указатели на QueueItem, а если объявлены только указатели на некоторый тип, то конкретизировать соответствующий класс не обязательно, хотя QueueItem вложен в шаблон класса Queue. QueueItem конкретизируется только тогда, когда указатели front или back разыменовываются в функциях-членах класса Queue.
 Внутри шаблона класса можно также объявлять перечисления и определять типы (с помощью typedef):
 template
 class Buffer:
 public:
  enum Buf_vals { last = size-1, Buf_size };
  typedef Type BufType;
  BufType array[ size ];
  // ...
 }
 Вместо того чтобы явно включать член Buf_size, в шаблоне класса Buffer объявляется перечисление с двумя элементами, которые инициализируются значением параметра шаблона. Например, объявление
 Buffer small_buf;
 устанавливает Buf_size в 512, а last – в 511. Аналогично
 Buffer medium_buf;
 устанавливает Buf_size в 1024, а last – в 1023.
 Открытый вложенный тип разрешается использовать и вне определения объемлющего класса. Однако вызывающая программа может ссылаться лишь на конкретизированные экземпляры подобного типа (или элементов вложенного перечисления). В таком случае имени вложенного типа должно предшествовать имя конкретизированного шаблона класса:
 // ошибка: какая конкретизация Buffer?
 Buffer::Buf_vals bfv0;
 
 Buffer::Buf_vals bfv1; // правильно
 Это правило применимо и тогда, когда во вложенном типе не используются параметры включающего шаблона:
 template class Q {
 public:
  enum QA { empty, full }; // не зависит от параметров
  QA status;
  // ...
 };
 
 #include
 
 int main() {
  Q qd;
  Q qi;
 
  qd.status = Q::empty; // ошибка: какая конкретизация Q?
  qd.status = Q::empty; // правильно
 
  int val1 = Q::empty;
  int val2 = Q::empty;
  if ( val1 != val2 )
  cerr << "ошибка реализации!" << endl;
  return 0;
 }
 Во всех конкретизациях Q значения empty одинаковы, но при ссылке на empty необходимо указывать, какому именно экземпляру Q принадлежит перечисление.
 Упражнение 16.8
 Определите класс List и вложенный в него ListItem из раздела 13.10 как шаблоны. Реализуйте аналогичные определения для ассоциированных членов класса.
 16.7. Шаблоны-члены
 Шаблон функции или класса может быть членом обычного класса или шаблона класса. Определение шаблона-члена похоже на определение шаблона: ему предшествует ключевое слово template, за которым идет список параметров:
 template
 class Queue {
 private:
  // шаблон класса-члена
  template
  class CL
  {
  Type member;
  T mem;
  };
  // ...
 public:
  // шаблон функции-члена
  template
  void assign( Iter first, Iter last )
  {
  while ( ! is_empty() )
  remove(); // вызывается Queue::remove()
 
  for ( ; first != last; ++first )
  add( *first ); // вызывается Queue::add( const T & )
  }
 }
 (Отметим, что шаблоны-члены не поддерживаются компиляторами, написанными до принятия стандарта C++. Эта возможность была добавлена в язык для поддержки реализации абстрактных контейнерных типов, представленных в главе 6.)
 Объявление шаблона-члена имеет собственные параметры. Например, у шаблона класса CL есть параметр Type, а у шаблона функции assign() – параметр Iter. Помимо этого, в определении шаблона-члена могут использоваться параметры объемлющего шаблона класса. Например, у шаблона CL есть член типа T, представляющего параметр включающего шаблона Queue.
 Объявление шаблона-члена в шаблоне класса Queue означает, что конкретизация Queue потенциально может содержать бесконечное число различных вложенных классов CL функций-членов assign(). Так, конкретизированный экземпляр Queue включает вложенные типы:
 Queue::CL
 Queue::CL
 и вложенные функции:
 void Queue::assign( int *, int * )
 void Queue::assign( vector::iterator,
  vector::iterator )
 Для шаблона-члена действуют те же правила доступа, что и для других членов класса. Так как шаблон CL является закрытым членом шаблона Queue, то лишь функции-члены и друзья Queue могут ссылаться на его конкретизации. С другой стороны, шаблон функции assign() объявлен открытым членом и, значит, доступен во всей программе.
 Шаблон-член конкретизируется при его использовании в программе. Например, assign() конкретизируется в момент обращения к ней из main():
 int main()
 {
  // конкретизация Queue
  Queue qi;
 
  // конкретизация Queue::assign( int *, int * )
  int ai[4] = { 0, 3, 6, 9 };
  qi.assign( ai, ai + 4 );
 
  // конкретизация Queue::assign( vector::iterator,
  // vector::iterator )
  vector vi( ai, ai + 4 );
  qi.assign( vi.begin(), vi.end() );
 }
 Шаблон функции assign(), являющийся членом шаблона класса Queue, иллюстрирует необходимость применения шаблонов-членов для поддержки контейнерных типов. Предположим, имеется очередь типа Queue, в которую нужно поместить содержимое любого другого контейнера (списка, вектора или обычного массива), причем его элементы имеют либо тип int (т.е. тот же, что у элементов очереди), либо приводимый к типу int. Шаблон-член assign()позволяет это сделать. Поскольку может быть использован любой контейнерный тип, то интерфейс assign() программируется в расчете на употребление итераторов; в результате реализация оказывается не зависящей от фактического типа, на который итераторы указывают.
 В функции main() шаблон-член assign() сначала конкретизируется типом int*, что позволяет поместить в qi содержимое массива элементов типа int. Затем шаблон-член конкретизируется типом vector::iterator – это дает возможность поместить в очередь qi содержимое вектора элементов типа int. Контейнер, содержимое которого помещается в очередь, не обязательно должен состоять из элементов типа int. Разрешен любой тип, который приводится к int. Чтобы понять, почему это так, еще раз посмотрим на определение assign():
 template
  void assign( Iter first, Iter last )
 {
  // удалить все элементы из очереди
 
  for ( ; first != last; ++first )
  add( *first );
 }
 Вызываемая из assign() функция add() – это функция-член Queue::add(). Если Queue конкретизируется типом int, то у add() будет следующий прототип:

<< Пред.           стр. 76 (из 121)           След. >>

Список литературы по разделу