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

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

  int iobj = 65;
  int res = si + iobj; // ::operator+() - функция-кандидат
 }
 множество операторов, объявленных в пространстве имен, в котором определен тип операнда. Если операнд имеет тип класса и этот тип объявлен в пользовательском пространстве имен, то операторные функции, объявленные в том же пространстве и имеющие то же имя, что и использованный оператор, считаются кандидатами:
 namespace NS {
  class SmallInt { /* ... */ };
  SmallInt operator+ ( const SmallInt&, double );
 }
 
 int main() {
  // si имеет тип SmallInt:
  // этот класс объявлен в пространстве имен NS
  NS::SmallInt si(15);
 
  // NS::operator+() - функция-кандидат
  int res = si + 566;
  return 0;
 }
 Операнд si имеет тип класса SmallInt, объявленного в пространстве имен NS. Поэтому перегруженный operator+(const SmallInt, double), объявленный в том же пространстве, добавляется к множеству кандидатов;
 множество операторов, объявленных друзьями классов, к которым принадлежат операнды. Если операнд принадлежит к типу класса и в определении этого класса есть одноименные применяемому оператору функции-друзья, то они добавляются к множеству кандидатов:
 namespace NS {
  class SmallInt {
  friend SmallInt operator+( const SmallInt&, int )
  { /* ... */ }
  };
 }
 int main() {
  NS::SmallInt si(15);
 
  // функция-друг operator+() - кандидат
  int res = si + 566;
  return 0;
 }
 Операнд si имеет тип SmallInt. Операторная функция operator+(const SmallInt&, int), являющаяся другом этого класса, – член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS эта операторная функция не будет найдена. Однако при использовании operator+() с аргументом типа SmallInt функции-друзья, объявленные в области видимости этого класса, включаются в рассмотрение и добавляются к множеству кандидатов.
 Эти три множества операторных функций-кандидатов формируются точно так же, как и для вызовов обычных функций с аргументами типа класса. Однако при использовании операторного синтаксиса строятся еще два множества:
 множество операторов-членов, объявленных в классе левого операнда. Если такой операнд оператора operator+() имеет тип класса, то в множество функций-кандидатов включаются объявления operator+(), являющиеся членами этого класса:
 class myFloat {
  myFloat( double );
 };
 class SmallInt {
 public:
  SmallInt( int );
  SmallInt operator+ ( const myFloat & );
 };
 
 int main() {
  SmallInt si(15);
 
  int res = si + 5.66; // оператор-член operator+() - кандидат
 }
 Оператор-член SmallInt::operator+(const myFloat &), определенный в SmallInt, включается в множество функций-кандидатов для разрешения вызова operator+() в main();
 множество встроенных операторов. Учитывая типы, которые можно использовать со встроенным operator+(), кандидатами являются также:
 int operator+( int, int );
 double operator+( double, double );
 T* operator+( T*, I );
 T* operator+( I, T* );
 Первое объявление относится к встроенному оператору для сложения двух значений целых типов, второе – к оператору для сложения значений типов с плавающей точкой. Третье и четвертое соответствуют встроенному оператору сложения указательных типов, который используется для прибавления целого числа к указателю. Два последних объявления представлены в символическом виде и описывают целое семейство встроенных операторов, которые могут быть выбраны компилятором на роль кандидатов при обработке операций сложения.
 Любое из первых четырех множеств может оказаться пустым. Например, если среди членов класса SmallInt нет функции с именем operator+(), то четвертое множество будет пусто.
 Все множество операторных функций-кандидатов является объединением пяти подмножеств, описанных выше:
 namespace NS {
  class myFloat {
  myFloat( double );
  };
  class SmallInt {
  friend SmallInt operator+( const SmallInt &, int ) { /* ... */ }
  public:
  SmallInt( int );
  operator int();
  SmallInt operator+ ( const myFloat & );
  // ...
  };
  SmallInt operator+ ( const SmallInt &, double );
 }
 
 int main() {
  // тип si - class SmallInt:
  // Этот класс объявлен в пространстве имен NS
  NS::SmallInt si(15);
 
  int res = si + 5.66; // какой operator+()?
  return 0;
 }
 В эти пять множеств входят семь операторных функций-кандидатов на роль operator+() в main():
 первое множество пусто. В глобальной области видимости, а именно в ней употреблен operator+() в функции main(), нет объявлений перегруженного оператора operator+();
 второе множество содержит операторы, объявленные в пространстве имен NS, где определен класс SmallInt. В этом пространстве имеется один оператор:
 NS::SmallInt NS::operator+( const SmallInt &, double );
 третье множество содержит операторы, объявленные друзьями класса SmallInt. Сюда входит
 NS::SmallInt NS::operator+( const SmallInt &, int );
 четвертое множество содержит операторы, объявленные членами SmallInt. Такой тоже есть:
 NS::SmallInt NS::SmallInt::operator+( const myFloat & );
 пятое множество содержит встроенные бинарные операторы:
 int operator+( int, int );
 double operator+( double, double );
 T* operator+( T*, I );
 T* operator+( I, T* );
 Да, формирование множества кандидатов для разрешения оператора, использованного с применением операторного синтаксиса, утомительно. Но после того как оно построено, устоявшие функции и наилучшая из них находятся, как и прежде, путем анализа преобразований, применимых к операндам отобранных кандидатов.
 15.12.2. Устоявшие функции
 Множество устоявших операторных функций формируется из множества кандидатов путем отбора лишь тех операторов, которые могут быть вызваны с заданными операндами. Например, какие из семи найденных выше кандидатов устоят? Оператор использован в следующем контексте:
 NS::SmallInt si(15);
 si + 5.66;
 Левый операнд имеет тип SmallInt, а правый – double.
 Первый кандидат является устоявшей функцией для данного использования operator+():
 NS::SmallInt NS::operator+( const SmallInt &, double );
 Левый операнд типа SmallInt в качестве инициализатора точно соответствует формальному параметру-ссылке этого перегруженного оператора. Правый, имеющий тип double, также точно соответствует второму формальному параметру.
 Следующая функция-кандидат также устоит:
 NS::SmallInt NS::operator+( const SmallInt &, int );
 Левый операнд si типа SmallInt в качестве инициализатора точно соответствует формальному параметру-ссылке перегруженного оператора. Правый имеет тип int и может быть приведен к типу второго формального параметра с помощью стандартного преобразования.
 Устоит и третья функция-кандидат:
 NS::SmallInt NS::SmallInt::operator+( const myFloat & );
 Левый операнд si имеет тип SmallInt, т.е. тип того класса, членом которого является перегруженный оператор. Правый имеет тип int и приводится к типу класса myFloat с помощью определенного пользователем преобразования в виде конструктора myFloat(double).
 Четвертой и пятой устоявшими функциями являются встроенные операторы:
 int operator+( int, int );
 double operator+( double, double );
 Класс SmallInt содержит конвертер, который может привести значение типа SmallInt к типу int. Этот конвертер используется вместе с первым встроенным оператором для преобразования левого операнда в тип int. Второй операнд типа double трансформируется в тип int с помощью стандартного преобразования. Что касается второго встроенного оператора, то конвертер приводит левый операнд от типа SmallInt к типу int, после чего результат стандартно преобразуется в double. Второй же операнд типа double точно соответствует второму параметру.
 Лучшей из этих пяти устоявших функций является первая, operator+(), объявленная в пространстве имен NS:
 NS::SmallInt NS::operator+( const SmallInt &, double );
 Оба ее операнда точно соответствуют параметрам.
 15.12.3. Неоднозначность
 Наличие в одном и том же классе конвертеров, выполняющих неявные преобразования во встроенные типы, и перегруженных операторов может приводить к неоднозначности при выборе между ними. Например, есть следующее определение класса String с функцией сравнения:
 class String {
  // ...
 public:
  String( const char * = 0 );
  bool operator== ( const String & ) const;
  // нет оператора operator== ( const char * )
 };
 и такое использование оператора operator==:
 String flower( "tulip" );
 void foo( const char *pf ) {
  // вызывается перегруженный оператор String::operator==()
  if ( flower == pf )
  cout << pf << " is a flower!\en";
  // ...
 }
 Тогда при сравнении
 flower == pf
 вызывается оператор равенства класса String:
 String::operator==( const String & ) const;
 Для трансформации правого операнда pf из типа const char* в тип String параметра operator==() применяется определенное пользователем преобразование, которое вызывает конструктор:
 String( const char * )
 Если добавить в определение класса String конвертер в тип const char*:
 class String {
  // ...
 public:
  String( const char * = 0 );
  bool operator== ( const String & ) const;
  operator const char*(); // новый конвертер
 };
 то показанное использование operator==() становится неоднозначным:
 // проверка на равенство больше не компилируется!
 if (flower == pf)
 Из-за добавления конвертера operator const char*() встроенный оператор сравнения
 bool operator==( const char *, const char * )
 тоже считается устоявшей функцией. С его помощью левый операнд flower типа String может быть преобразован в тип const char *.
 Теперь для использования operator==() в foo() есть две устоявших операторных функции. Первая из них
 String::operator==( const String & ) const;
 требует применения определенного пользователем преобразования правого операнда pf из типа const char* в тип String. Вторая
 bool operator==( const char *, const char * )
 требует применения пользовательского преобразования левого операнда flower из типа String в тип const char*.
 Таким образом, первая устоявшая функция лучше для левого операнда, а вторая – для правого. Поскольку наилучшей функции не существует, то вызов помечается компилятором как неоднозначный.
 При проектировании интерфейса класса, включающего объявление перегруженных операторов, конструкторов и конвертеров, следует быть весьма аккуратным. Определенные пользователем преобразования применяются компилятором неявно. Это может привести к тому, что встроенные операторы окажутся устоявшими при разрешении перегрузки для операторов с операндами типа класса.
 Упражнение 15.17
 Назовите пять множеств функций-кандидатов, рассматриваемых при разрешении перегрузки оператора с операндами типа класса.
 Упражнение 15.18
 Какой из операторов operator+() будет выбран в качестве наилучшего из устоявших для оператора сложения в main()? Перечислите все функции-кандидаты, все устоявшие функции и преобразования типов, которые надо применить к аргументам для каждой устоявшей функции.
 namespace NS {
  class complex {
  complex( double );
  // ...
  };
  class LongDouble {
  friend LongDouble operator+( LongDouble &, int ) { /* ... */ }
  public:
  LongDouble( int );
  operator double();
  LongDouble operator+( const complex & );
  // ...
  };
  LongDouble operator+( const LongDouble &, double );
 }
 
 int main() {
  NS::LongDouble ld(16.08);
 
  double res = ld + 15.05; // какой operator+?
  return 0;
 }16
 16. Шаблоны классов
 В этой главе описывается, как определять и использовать шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов либо значений параметризованы. Начинающий программист может использовать шаблоны, не понимая механизма, стоящего за их определениями и конкретизациями. Фактически на протяжении всей этой книги мы пользовались шаблонами классов, которые определены в стандартной библиотеке C++ (например, vector, list и т.д.), и при этом не нуждались в детальном объяснении механизма их работы. Только профессиональные программисты определяют собственные шаблоны классов и пользуются описанными в данной главе средствами. Поэтому этот материал следует рассматривать как введение в более сложные аспекты C++.
 Глава 16 содержит вводные и продвинутые разделы. Во вводных разделах показано, как определяются шаблоны классов, иллюстрируются простые способы применения и обсуждается механизм их конкретизации. Мы расскажем, как можно задавать в шаблонах разные виды членов: функции-члены, статические данные-члены и вложенные типы. В продвинутых разделах представлен материал, необходимый для написания приложений промышленного уровня. Сначала мы рассмотрим, как компилятор конкретизирует шаблоны и какие требования в связи с этим предъявляются к организации нашей программы. Затем покажем, как определять специализации и частичные специализации для шаблона класса и для его члена. Далее мы остановимся на двух вопросах, представляющих интерес для проектировщиков: как разрешаются имена в определениях шаблона класса и как можно определять шаблоны в пространствах имен. Завершается эта глава примером определения и использования шаблона класса.
 16.1. Определение шаблона класса
 Предположим, что нам нужно определить класс, поддерживающий механизм очереди. Очередь – это структура данных для хранения коллекции объектов; они помещаются в конец очереди, а извлекаются из ее начала. Поведение очереди описывают аббревиатурой FIFO – “первым пришел, первым ушел”. (Определенный в стандартной библиотеке C++ тип, реализующий очередь, упоминался в разделе 6.17. В этой главе мы создадим упрощенный тип для знакомства с шаблонами классов.)
 Необходимо, чтобы наш класс Queue поддерживал следующие операции:
 добавить элемент в конец очереди:
 void add( item );
 удалить элемент из начала очереди:
 item remove();
 определить, пуста ли очередь:
 bool is_empty();
 определить, заполнена ли очередь:
 bool is_full();
 Определение Queue могло бы выглядеть так:
 class Queue {
 public:
  Queue();
  ~Queue();
 
  Type& remove();
  void add( const Type & );
  bool is_empty();
  bool is_full();
 private:
  // ...
 };
 Вопрос в том, какой тип использовать вместо 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 Queue {
 public:
  Queue();
  ~Queue();
 
  Type& remove();
  void add( const Type & );
  bool is_empty();
  bool is_full();
 private:
  // ...
 };
 Чтобы создать классы Queue, способные хранить целые числа, комплексные числа и строки, программисту достаточно написать:
 Queue qi;
 Queue< complex > qc;
 Queue qs;
 Реализация Queue представлена в следующих разделах с целью иллюстрации определения и применения шаблонов классов. В реализации используются две абстракции шаблона:
 сам шаблон класса Queue предоставляет описанный выше открытый интерфейс и пару членов: front и back. Очередь реализуется с помощью связанного списка;
 шаблон класса QueueItem представляет один узел связанного списка Queue. Каждый помещаемый в очередь элемент сохраняется в объекте QueueItem, который содержит два члена: value и next. Тип value будет различным в каждом экземпляре класса Queue, а next – это всегда указатель на следующий объект QueueItem в очереди.
 Прежде чем приступать к детальному изучению реализации этих шаблонов, рассмотрим, как они объявляются и определяются. Вот объявление шаблона класса QueueItem:
 template
  class QueueItem;
 Как объявление, так и определение шаблона всегда начинаются с ключевого слова template. За ним следует заключенный в угловые скобки список параметров шаблона, разделенных запятыми. Список не бывает пустым. В нем могут быть параметры-типы, представляющие некоторый тип, и параметры-константы, представляющие некоторое константное выражение.
 Параметр-тип шаблона состоит из ключевого слова class или typename (в списке параметров они эквивалентны), за которым следует идентификатор. (Ключевое слово typename не поддерживается компиляторами, написанными до принятия стандарта C++. В разделе 10.1 подробно объяснялось, зачем это слово было добавлено в язык.) Оба ключевых слова обозначают, что последующее имя параметра относится к встроенному или определенному пользователем типу. Например, в приведенном выше определении шаблона QueueItem имеется один параметр-тип T. Допустимым фактическим аргументом для T является любой встроенный или определенный пользователем тип, такой, как int, double, char*, complex или string.
 У шаблона класса может быть несколько параметров-типов:
 template
  class Container;
 Однако ключевое слово class или typename должно предшествовать каждому. Следующее объявление ошибочно:
 // ошибка: должно быть или
 //
 template
  class collection;
 Объявленный параметр-тип служит спецификатором типа в оставшейся части определения шаблона и употребляется точно так же, как любой встроенный или определенный пользователем тип в обычном определении класса. Например, параметр-тип можно использовать для объявления данных и функций-членов, членов вложенных классов и т.д.
 Не являющийся типом параметр шаблона представляет собой обычное объявление. Он показывает, что следующее за ним имя – это потенциальное значение, употребляемое в определении шаблона в качестве константы. Так, шаблон класса Buffer может иметь параметр-тип, представляющий типы элементов, хранящихся в буфере, и параметр-константу, содержащий его размер:
 template
  class Buffer;
 За списком параметров шаблона следует определение или объявление класса. Шаблон определяется так же, как обычный класс, но с указанием параметров:
 template
 class QueueItem {
 public:
  // ...
 private:
  // Type представляет тип члена
  Type item;
  QueueItem *next;
 };
 В этом примере Type используется для обозначения типа члена item. По ходу выполнения программы вместо Type могут быть подставлены различные встроенные или определенные пользователем типы. Такой процесс подстановки называется конкретизацией шаблона.
 Имя параметра шаблона можно употреблять после его объявления и до конца объявления или определения шаблона. Если в глобальной области видимости объявлена переменная с таким же именем, как у параметра шаблона, то это имя будет скрыто. В следующем примере тип item равен не double, а типу параметра:
 typedef double Type;
 
 template
 class QueueItem {
 public:
  // ...
 private:
  // тип Item - не double
  Type item;
  QueueItem *next;
 };
 Член класса внутри определения шаблона не может быть одноименным его параметру:
 template
 class QueueItem {
 public:
  // ...
 private:
  // ошибка: член не может иметь то же имя, что и
  // параметр шаблона Type
  typedef double Type;
  Type item;
  QueueItem *next;
 };
 Имя параметра шаблона может встречаться в списке только один раз. Поэтому следующее объявление компилятор помечает как ошибку:
 // ошибка: неправильное использование имени параметра шаблона Type
 template
  class container;

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

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