Язык программирования C++. Вводный курс

Объявления друзей в шаблонах Queue и QueueItem


Поскольку QueueItem не предназначен для непосредственного использования в вызывающей программе, то объявление конструктора этого класса помещено в закрытую секцию шаблона. Теперь класс Queue

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

Существует два способа объявить шаблон класса другом. Первый заключается в том, чтобы объявить любой экземпляр Queue

другом любого экземпляра QueueItem:



template <class Type>

class QueueItem {

   // любой экземпляр Queue является другом

   // любого экземпляра QueueItem

   template <class T> friend class Queue;

};

Однако нет смысла объявлять, например, класс 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

класса Queue. Поэтому оператор необходимо объявить другом Queue:



template <class Type>

class Queue {

   friend ostream&

      operator<<( ostream &, const Queue<Type> & );

   // ...
};

Здесь, как и при объявлении друга в шаблоне класса 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, оператор следует объявить другом шаблона QueueItem. Это делается следующим образом:



template <class Type>

class QueueItem {

   friend class Queue<Type>;

   friend ostream&

      operator<<( ostream &, const QueueItem<Type> & );

   // ...
};

Оператор вывода класса 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\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, добавленных в его шаблон.


Содержание раздела