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

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

  // подробности см. в разделе 17.7
 
  // query - это член класса TextQuery типа Query*
  query = user_query.eval_query();
 
  // вычислить иерархию Query,
  // реализация описана в разделе 17.7
  query->eval();
 
  // вывести ответ с помощью
  // функции-члена класса TextQuery
  display_solution();
 
  // вывести на терминал пользователя дополнительную
  // пустую строку
  cout << endl;
  }
  }
  while ( ! query_text.empty() );
  cout << "До свидания!\n";
 }
 Тестируя программу, мы применили ее к нескольким текстам. Первым стал короткий рассказ Германа Мелвилла “Bartleby”. Здесь иллюстрируется составной запрос AndQuery, для которого подходящие слова расположены в соседних строках. (Отметим, что слова, заключенные между символами косой черты, предполагаются набранными курсивом.)
 
 Введите запрос. Пожалуйста, разделяйте все его элементы пробелами.
 Запрос (или весь сеанс) завершается точкой ( . ).
 ==> John && Jacob && Astor
 
  john ( 3 ) lines match
  jacob ( 3 ) lines match
  john && jacob ( 3 ) lines match
  astor ( 3 ) lines match
  john && jacob && astor ( 5 ) lines match
 
 Requested query: john && jacob && astor
 ( 34 ) All who know me consider me an eminently /safe/ man. The late
 John Jacob
 ( 35 ) Astor, a personage little given to poethic enthusiasm, had no
 hesitation in
 ( 38 ) my profession by the late John Jacob Astor, a name which, I admit
 I love to
 ( 40 ) bullion. I will freely add that I was not insensible to the late
 John Jacob
 ( 41 ) Astor's good opinion.
 
 Следующий запрос, в котором тестируются скобки и составные операторы, обращен к тексту новеллы “Heart of Darkness” Джозефа Конрада:
 
 ==> horror || ( absurd && mystery ) || ( North && Pole )
 
  horror ( 5 ) lines match
  absurd ( 8 ) lines match
  mystery ( 12 ) lines match
  ( absurd && mystery ) ( 1 ) lines match
  horror || ( absurd && mystery ) ( 6 ) lines match
  north ( 2 ) lines match
  pole ( 7 ) lines match
  ( north && pole ) ( 1 ) lines match
  horror || ( absurd && mystery ) || ( north && pole )
  ( 7 ) lines match
 
 Requested query: horror || ( absurd && mystery ) || ( north && pole )
 ( 257 ) up I will go there.' The North Pole was one of these
 ( 952 ) horros. The heavy pole had skinned his poor nose
 ( 3055 ) some lightless region of subtle horrors, where pure,
 ( 3673 ) " 'The horror! The horror!'
 ( 3913 ) the whispered cry, 'The horror! The horror! '
 ( 3957 ) absurd mysteries not fit for a human being to behold.
 ( 4088 ) wind. 'The horror! The horror!'
 
 Последний запрос был обращен к отрывку из романа Генри Джеймса “Portrait of a Lady”. В нем иллюстрируется составной запрос в применении к большому текстовому файлу:
 
 ==> clever && trick || devious
 
  clever ( 46 ) lines match
  trick ( 12 ) lines match
  clever && trick ( 2 ) lines match
  devious ( 1 ) lines match
  clever && trick || devious ( 3 ) lines match
 
 Requested query: clever && trick || devious
 ( 13914 ) clever trick she had guessed. Isabel, as she herself grew older
 ( 13935 ) lost the desire to know this lady's clever trick. If she had
 ( 14974 ) desultory, so devious, so much the reverse of processional.
 There were
 
 Упражнение 17.23
 Реализованная нами обработка запроса пользователя обладает одним недостатком: она не применяет к каждому слову те же предварительные фильтры, что и программа, строящая вектор позиций (см. разделы 6.9 и 6.10). Например, пользователь, который хочет найти слово “maps”, обнаружит, что в нашем представлении текста распознается только “map”, поскольку существительные во множественном числе приводятся к форме в единственном числе. Модифицируйте функцию query_text() так, чтобы она применяла эквивалентные фильтры к словам запроса.
 Упражнение 17.24
 Поисковую систему можно было бы усовершенствовать, добавив еще одну разновидность запроса “И”, которую мы назовем InclusiveAndQuery и будем обозначать символом &. Строка текста удовлетворяет условиям запроса, если в ней находятся оба указанных слова, пусть даже не рядом. Например, строка
 
 We were her pride of ten, she named us
 
 удовлетворяет запросу:
 
 pride & ten
 
 но не:
 
 pride && ten
 
 Поддержите запрос InclusiveAndQuery.
 Упражнение 17.25
 Представленная ниже реализация функции display_solution() может выводить только в стандартный вывод. Более правильно было бы позволить пользователю самому задавать поток ostream, в который надо направить вывод. Модифицируйте display_solution() так, чтобы ostream можно было задавать. Какие еще изменения необходимо внести в определение класса UserQuery?
 void TextQuery::
 display_solution()
 {
  cout << "\n"
  << "Requested query: "
  << *query << "\n\n";
 
  const set,allocator> *solution = query->solution();
 
  if ( ! solution->size() ) {
  cout << "\n\t"
  << "Sorry, no matching lines were found in text.\n"
  << endl;
  }
 
  set::const_iterator
  it = solution->begin(),
  end_it = solution->end();
 
  for ( ; it != end_it; ++it ) {
  int line = *it;
 
  // пронумеруем строки с 1 ...
  cout << "( " << line+1 << " ) "
  << (*lines_of_text)[line] << '\n';
  }
  cout << endl;
 }
 Упражнение 17.26
 Нашему классу TextQuery не хватает возможности принимать аргументы, заданные пользователем в командной строке.
 Предложите синтаксис командной строки для нашей поисковой системы.
 Добавьте в класс необходимые данные и функции-члены.
 Предложите средства для работы с командной строкой (см. пример в разделе 7.8).
 Упражнение 17.27
 В качестве темы для рабочего проекта рассмотрите следующие усовершенствования нашей поисковой системы:
 Реализуйте поддержку, необходимую для представления запроса AndQuery в виде одной строки, например “Motion Picture Screen Cartoonists”.
 Реализуйте поддержку для ответа на запрос на основе вхождения слов не в строку, а в предложение.
 Реализуйте подсистему хранения истории, с помощью которой пользователь мог бы ссылаться на предыдущий запрос по номеру, возможно, комбинируя его с новым запросом.
 Вместо того чтобы показывать счетчик найденных и все найденные строки, реализуйте возможность задать диапазон выводимых строк для промежуточных вычислений и для окончательного ответа:
 
 ==> John && Jacob && Astor
 
 (1) john ( 3 ) lines match
 (2) jacob ( 3 ) lines match
 (3) john && jacob ( 3 ) lines match
 (4) astor ( 3 ) lines match
 (5) john && jacob && astor ( 5 ) lines match
 
 // Новая возможность: пусть пользователь укажет, какой запрос выводить
 // пользователь вводит число
 ==> вывести? 3
 
 // Затем система спрашивает, сколько строк выводить
 // при нажатии клавиши Enter выводятся все строки,
 // но пользователь может также ввести номер одной строки или диапазон
 сколько (Enter выводит все, иначе введите номер строки или диапазон) 1-3
 
 18
 18. Множественное и виртуальное наследование
 В большинстве реальных приложений на C++ используется открытое наследование от одного базового класса. Можно предположить, что и в наших программах оно в основном будет применяться именно так. Но иногда одиночного наследования не хватает, потому что с его помощью либо нельзя адекватно смоделировать абстракцию предметной области, либо получающаяся модель чересчур сложна и неинтуитивна. В таких случаях следует предпочесть множественное наследование или его частный случай – виртуальное наследование. Их поддержка, имеющаяся в C++, – основная тема настоящей главы.
 18.1. Готовим сцену
 Прежде чем детально описывать множественное и виртуальное наследование, покажем, зачем оно нужно. Наш первый пример взят из области трехмерной компьютерной графики. Но сначала познакомимся с предметной областью.
 В компьютере сцена представляется графом сцены, который содержит информацию о геометрии (трехмерные модели), один или более источников освещения (иначе сцена будет погружена во тьму), камеру (без нее мы не можем смотреть на сцену) и несколько трансформационных узлов, с помощью которых позиционируются элементы.
 Процесс применения источников освещения и камеры к геометрической модели для получения двумерного изображения, отображаемого на дисплее, называется рендерингом. В алгоритме рендеринга учитываются два основных аспекта: природа источника освещения сцены и свойства материалов поверхностей объектов, такие, как цвет, шероховатость и прозрачность. Ясно, что перышки на белоснежных крыльях феи выглядят совершенно не так, как капающие из ее глаз слезы, хотя те и другие освещены одним и тем же серебристым светом.
 Добавление объектов к сцене, их перемещение, игра с источниками освещения и геометрией – работа компьютерного художника. Наша задача – предоставить интерактивную поддержку для манипуляций с графом сцены на экране. Предположим, что в текущей версии своего инструмента мы решили воспользоваться каркасом приложений Open Inventor для C++ (см. [WERNECKE94]), но с помощью подтипизации расширили его, создав собственные абстракции нужных нам классов. Например, Open Inventor располагает тремя встроенными источниками освещения, производными от абстрактного базового класса SoLight:
 class SoSpotLight : public SoLight { ... }
 class SoPointLight : public SoLight { ... }
 class SoDirectionalLight : public SoLight { ... }
 Префикс So служит для того, чтобы дать уникальные имена сущностям, которые в области компьютерной графики весьма распространены (данный каркас приложений проектировался еще до появления пространств имен). Точечный источник (point light) – это источник света, излучающий, как солнце, во всех направлениях. Направленный источник (directional light) – источник света, излучающий в одном направлении. Прожектор (spotlight) – источник, испускающий узконаправленный конический пучок, как обычный театральный прожектор.
 По умолчанию Open Inventor осуществляет рендеринг графа сцены на экране с помощью библиотеки OpenGL (см. [NEIDER93]). Для интерактивного отображения этого достаточно, но почти все изображения, сгенерированные для киноиндустрии, сделаны с помощью средства RenderMan (см. [UPSTILL90]). Чтобы добавить поддержку такого алгоритма рендеринга мы, в частности, должны реализовать собственные специальные подтипы источников освещения:
 class RiSpotLight : public SoSpotLight { ... }
 class RiPointLight : public SoPointLight { ... }
 class RiDirectionalLight : public SoDirectionalLight { ... }
 Новые подтипы содержат дополнительную информацию, необходимую для рендеринга с помощью RenderMan. При этом базовые классы Open Inventor по-прежнему позволяют выполнять рендеринг с помощью OpenGL. Неприятности начинаются, когда возникает необходимость расширить поддержку теней.
 В RenderMan направленный источник и прожектор поддерживают отбрасывание тени (поэтому мы называем их источниками освещения, дающими тень, – SCLS), а точечный – нет. Общий алгоритм требует, чтобы мы обошли все источники освещения на сцене и составили карту теней для каждого включенного SCLS. Проблема в том, что источники освещения хранятся в графе сцены как полиморфные объекты класса SoLight. Хотя мы можем инкапсулировать общие данные и необходимые операции в класс SCLS, непонятно, как включить его в существующую иерархию классов Open Inventor.
 В поддереве с корнем SoLight в иерархии Open Inventor нет такого класса, из которого можно было бы произвести с помощью одиночного наследования класс SCLS так, чтобы в дальнейшем уже от него произвести SdRiSpotLight и SdRiDirectionalLight. Если не пользоваться множественным наследованием, лучшее, что можно сделать, – это сравнить член класса SCLS с каждым возможным типом SCLS-источника и вызвать соответствующую операцию:
 SoLight *plight = next_scene_light();
 
 if ( RiDirectionalLight *pdilite =
  dynamic_cast( plight ))
  pdilite->scls.cast_shadow_map();
 else
 if ( RiSpotLight *pslite =
  dynamic_cast( plight ))
  pslite->scls.cast_shadow_map();
 // и так далее
 (Оператор dynamic_cast – это часть механизма идентификации типов во время выполнения (RTTI). Он позволяет опросить тип объекта, адресованного полиморфным указателем или ссылкой. Подробно RTTI будет обсуждаться в главе 19.)
 Пользуясь множественным наследованием, мы можем инкапсулировать подтипы SCLS, защитив наш код от изменений при добавлении или удалении источника освещения (см. рис. 18.1).
 
  SoNode SCLS
 
 
  SoLight
 
 
  SoPointLight SoSpotLight SoDirectionalLight
 
 
  RPointLight RSpotLight RDirectionalLight
 Рис. 18.1. Множественное наследование источников освещения
 class RiDirectionalLight :
  public SoDirectionalLight, public SCLS { ... };
 
 class RiSpotLight :
  public SoSpotLight, public SCLS { ... };
 
 // ...
 SoLight *plight = next_scene_light();
 if ( SCLS *pscls = dynamic_cast(plight))
  pscls->cast_shadow_map();
 Это решение несовершенно. Если бы у нас был доступ к исходным текстам Open Inventor, то можно было бы избежать множественного наследования, добавив к SoLight член-указатель на SCLS и поддержку операции cast_shadow_map():
 class SoLight : public SoNode {
 public:
  void cast_shadow_map()
  { if ( _scls ) _scls->cast_shadow_map(); }
  // ...
 protected:
  SCLS *_scls;
 };
 
 // ...
 
 SdSoLight *plight = next_scene_light();
 plight-> cast_shadow_map();
 Самое распространенное приложение, где используется множественное (и виртуальное) наследование, – это потоковая библиотека ввода/вывода в стандартном C++. Два основных видимых пользователю класса этой библиотеки – istream (для ввода) и ostream (для вывода). В число их общих атрибутов входят:
 информация о форматировании (представляется ли целое число в десятичной, восьмеричной или шестнадцатеричной системе счисления, число с плавающей точкой – в нотации с фиксированной точкой или в научной нотации и т.д.);
 информация о состоянии (находится ли потоковый объект в нормальном или ошибочном состоянии и т.д.);
 информация о параметрах локализации (отображается ли в начале даты день или месяц и т.д.);
 буфер, где хранятся данные, которые нужно прочитать или записать.
 Эти общие атрибуты вынесены в абстрактный базовый класс ios, для которого istream и ostream являются производными.
 Класс iostream – наш второй пример множественного наследования. Он предоставляет поддержку для чтения и записи в один и тот же файл; его предками являются классы istream и ostream. К сожалению, по умолчанию он также унаследует два различных экземпляра базового класса ios, а нам это не нужно.
 Виртуальное наследование решает проблему наследования нескольких экземпляров базового класса, когда нужен только один разделяемый экземпляр. Упрощенная иерархия iostream изображена на рис. 18.2.
 
  ios
 
  istream ostream
 
 
  ifstream iostream ofstream
 
 
  fstream
 
 Рис. 18.2. Иерархия виртуального наследования iostream (упрощенная)
 Еще один реальный пример виртуального и множественного наследования дают распределенные объектные вычисления. Подробное рассмотрение этой темы см. в серии статей Дугласа Шмидта (Douglas Schmidt) и Стива Виноски (Steve Vinoski) в [LIPPMAN96b].
 В данной главе мы рассмотрим использование и поведение механизмов виртуального и множественного наследования. В другой нашей книге, “Inside the C++ Object Model”, описаны более сложные вопросы производительности и дизайна этого аспекта языка.
 Для последующего обсуждения мы выбрали иерархию животных в зоопарке. Наши животные существуют на разных уровнях абстракции. Есть, конечно, особи, имеющие свои имена: Линь-Линь, Маугли или Балу. Каждое животное принадлежит к какому-то виду; скажем, Линь-Линь – это гигантская панда. Виды в свою очередь входят в семейства. Так, гигантская панда – член семейства медведей, хотя, как мы увидим в разделе 18.5, по этому поводу в зоологии долго велись бурные дискуссии. Каждое семейство – член животного мира, в нашем случае ограниченного территорией зоопарка.
 На каждом уровне абстракции имеются данные и операции, необходимые для поддержки все более и более широкого круга пользователей. Например, абстрактный класс ZooAnimal хранит информацию, общую для всех животных в зоопарке, и предоставляет открытый интерфейс для всех возможных запросов.
 Помимо классов, описывающих животных, есть и вспомогательные классы, инкапсулирующие различные абстракции иного рода, например “животные, находящиеся под угрозой вымирания”. Наша реализация класса Panda множественно наследует от Bear (медведь) и Endangered (вымирающие).
 18.2. Множественное наследование
 Для поддержки множественного наследования синтаксис списка базовых классов
 class Bear : public ZooAnimal { ... };
 расширяется: допускается наличие нескольких базовых классов, разделенных запятыми:
 class Panda : public Bear, public Endangered { ... };
 Для каждого из перечисленных базовых классов должен быть указан уровень доступа: public, protected или private. Как и при одиночном наследовании, множественно наследовать можно только классу, определение которого уже встречалось ранее.
 Язык не накладывает никаких ограничений на число базовых классов, которым может наследовать производный. На практике чаще всего встречается два класса, один из которых представляет открытый абстрактный интерфейс, а второй – закрытую реализацию (хотя ни один из рассмотренных выше примеров этой модели не следует). Производные классы, наследующие от трех или более базовых, – это пример такого стиля проектирования, когда каждый базовый класс представляет одну грань полного интерфейса производного.
 В случае множественного наследования объект производного класса содержит по одному подобъекту каждого из своих базовых (см. раздел 17.3). Например, когда мы пишем
 Panda ying_yang;
 то объект ying_yang будет состоять из подобъекта класса Bear (который в свою очередь содержит подобъект ZooAnimal), подобъекта Endangered и нестатических членов, объявленных в самом классе Panda, если таковые есть (см. рис. 18.3).
 
  ZooAnimal Endangered
 
 
  Bear
 
 
  Panda
 
 Рис. 18.3. Иерархия множественного наследования класса Panda
 Конструкторы базовых классов вызываются в порядке объявления в списке базовых классов. Например, для ying_yang эта последовательность такова: конструктор Bear (но поскольку класс Bear – производный от ZooAnimal, то сначала вызывается конструктор ZooAnimal), затем конструктор Endangered и в самом конце конструктор Panda.
 Как отмечалось в разделе 17.4, на порядок вызова не влияет ни наличие базовых классов в списке инициализации членов, ни порядок их перечисления. Иными словами, если бы конструктор Bear вызывался неявно и потому не был бы упомянут в списке инициализации членов, как в следующем примере:
 // конструктор по умолчанию класса Bear вызывается до
 // конструктора класса Endangered с двумя аргументами ...
 
 Panda::Panda()
  : Endangered( Endangered::environment,
  Endangered::critical )
 { ... }
 то все равно конструктор по умолчанию Bear был бы вызван раньше, чем явно заданный в списке конструктор класса Endangered с двумя аргументами.
 Порядок вызова деструкторов всегда противоположен порядку вызова конструкторов. В нашем примере деструкторы вызываются в такой последовательности: ~Panda(), ~Endangered(), ~Bear(), ~ZooAnimal().
 В разделе 17.3 уже говорилось, что в случае одиночного наследования к открытым и защищенным членам базового класса можно обращаться напрямую (не квалифицируя имя члена именем его класса), как если бы они были членами производного класса. То же самое справедливо и для множественного наследования. Однако при этом можно унаследовать одноименные члены из двух или более базовых классов. В таком случае прямое обращение оказывается неоднозначным и приводит к ошибке компиляции.
 Однако такую ошибку вызывает не потенциальная неоднозначность неквалифицированного доступа к одному из двух одноименных членов, а лишь попытка фактического обращения к нему (см. раздел 17.4). Например, если в обоих классах Bear и Endangered определена функция-член print(), то инструкция
 ying_yang.print( cout );
 приводит к ошибке компиляции, даже если у двух унаследованных функций-членов разные списки параметров.
 Error: ying_yang.print( cout ) -- ambiguous, one of
  Bear::print( ostream& )
  Endangered::print( ostream&, int )
 Ошибка: ying_yang.print( cout ) -- неоднозначно, одна из
  Bear::print( ostream& )
  Endangered::print( ostream&, int )
 Причина в том, что унаследованные функции-члены не образуют множество перегруженных функций внутри производного класса (см. раздел 17.3). Поэтому print() разрешается только по имени, а не по типам фактических аргументов. (О том, как производится разрешение, мы поговорим в разделе 18.4.)
 В случае одиночного наследования указатель, ссылка или объект производного класса при необходимости автоматически преобразуются в указатель, ссылку или объект базового класса, которому открыто наследует производный. Это остается верным и для множественного наследования. Так, указатель, ссылку или сам объект класса Panda можно преобразовать в указатель, ссылку или объект ZooAnimal, Bear или Endangered:
 extern void display( const Bear& );
 extern void highlight( const Endangered& );
 
 Panda ying_yang;
 
 display( ying_yang ); // правильно

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

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