А. Александреску - Современное проектирование на C++ (1119444), страница 63
Текст из файла (страница 63)
чог д оосивепс:: ог з р1ау э сас1зс(сз О ( оосвсасз зсасгзсгсз; тог (каждый элемент документа) ( элемент- лссерс(з\ас1зс1сз); ) всат(зс1сз.огзр1ауО; Проанализируем возникаюший контекст. Мы создали новую иерархию, корнем которой является класс оосе1евепсч1згсог. Это иерархия операций ()ггегагс)гу ог орегайопз). Каждый класс, входяший в нее, на самом деле представляет собой операцию, например оосэсасз. Теперь добавлять новые операции совсем несложно. Для этого нужно лишь создать новый класс, производный от класса оосе1евепсч151сог. Ни один элемент иерархии класса оосе1евепс для этого менять не требуется. Например, добавим одну операцию, скажем, тпсгевепсяопС51хе, связанную с горячей клавишей, предназначенной для увеличения размера шрифта на один пункт, или соответствуюшей кнопкой. с1аээ тпсгевепсяопс51хе : ри)г)1с оосе)евепсчгзгсог ( ри)г)г с: ч1гсиа1 чо1г) ч151сяагайгарп(рагайгар)гб раг) раг.эетропС51хе(раг.аеСЕоптвгхеО е 1) ч(гсиа1 чо1д ч1згсяазвегВгтвар(пазтегегтварй) // ничего не делаем Вот в все.
Не нужно вносить никаких изменений ни в иерархию класса оосе1евепс, ни в другие операции. Вы просто добавляете новый класс. Функция оосе1евепс::дссерс перебирает все объекты класса тпсгевепскопс51хе совершенно так же, как объекты класса оос5Сатз. Полученная в результате структура классов представлена на рнс.
10.1. Напомним, что по умолчанию новый класс легко добавить, а виртуальную функцию-член — нет. Мы преобразовали классы в функции с помошью перехода от иерархии класса оосе1евепс к иерархии класса оосе1евепсч151сог. Таким образом, классы, производные от класса оосе1евепсч1згсог, представляют собой вопгогцениые функции (оЬ)есггггег) гбзпс6опз). Так работает шаблон проектирования Чгзггог.
Часть Н. Компоненты Рис. гО.!. Иерархию классов для игры с двум» уровнями глогкноггяи 10.2. Перегрузка и функция-ловушка Управление перегрузкой функций в языке С++ оказывает очень большое влияние на реализацию проектов на основе шаблона ч1 з1сог, хотя лля самого шаблона перегрузка функций значения не имеет. В классе ооса1евепсч1зтсог каждому инспектируемому типу соответствует отдельная функция-член; чтв1срагадгарн(рагадгарнф), чтз1сяазсегв1сиар(аазсегвтсвара) и тл. Эти функции явно избыточны. Кроме тою, имя инспектируемого типа является частью самой функции. Как правило, избыточности лучше избегать, Для этого следует применять перегрузку функций.
Мы просто назовем все функции одним именем чтзчс и предоставим компилятору решать, какую перегруженную функцию чтз1с вызывать для передаваемого параметра заданного типа. Альтернативое определение класса оосв1евепсчт зз сог выглядит следуюшим образом. с1азв оосд1еиепсч1зтсог ( рцЫ1с: И гсиа1 чотб ч1ззс(рагадгарпб) О; Ч1ГСца1 Чптд ЧЗЗ1С(ааЗСЕГВбтаара) и О; ... другие подобные функции ... ): гЕ1 Глава 10. Шаблон Иа!тог Теперь можно проще определить функции-члены десерт, поскольку их повеление становится единообразным.
чо1б пагадгарЬ::десерт(оосв1евелсч151тогб ч) ч.ч151с(*т(115); чо16 казтегв1твар::десерт(оосв1евептч151тог8 ч) У.У151с(*тЬ15); Они выглядят настолько одинаковыми, что может возникнуть соблазн перенести их в базовый класс оосв1евепт. Это было бы ошибкой. На самом деле это совершенно разные функции. параметр 'т(115 в функции пагадгар(1::десерт имеет статический тип пагадгарМ, а в функции вазтегв1твар::десерт — тип аазтегв1тварб.
Именно эти статические типы помогают компилятору правильно определять, какую из перегруженных функций оосв1евептч151тог::ч151т следует вызывать. если бы функция десерт была реачизована в классе оосв1евепт„параметр *тЫ 5 иМел бы статический тип оосв1евептй, который не может предоставить компилятору всю необходимую информацию. Таким образом, факторизация классов не должна быть произвольной. Неправильная факторизация может привести программу в негодность. Перегрузка функций порождает интересную идею. Допустим, что все классы, производные от класса оосе1евепт, реализуют функцию десерт, просто переадресовывая вызов функции оосв1евептч151тог::У151т. Тогда в классе оосв1евепт можно определить следующую перегруженную функцию-ловушку (сатен-ай очег!сад). с1а55 посв)евептч151тог ( риЫ1с: как и прежде ч1гтца! чо1б У151т(оосв1евептб) = О; Когда будет вызываться эта перегруженная функция? Если новый класс является непосредственным наследником класса оосв1евепт и в классе оосв1евептч151тог Лля него нет подходящей перегруженной функции Ч151т, то вступают в силу правила перегрузки и автоматического преобразования производных типов в базовые.
Ссылка на объект неизвестного класса автоматически конвертируется в ссылку на объект базового класса оосв1евепт, и вызывается функция-ловушка. Если такой функции нет, возникает ошибка компиляции. Предусматривать функцию-ловушку или нет, зависит от конкретной ситуации. В перегруженной функции-ловушке можно выполнить много полезной работы. Примеры представлены в работах Влиссидеса (У!)55Ыез, !998, !999). В такой функции можно выполнять произвольные действия весьма обобщенного характера или прибегнуть в переключению типов (используя оператор г(упав1с сазе), чтобы распознать фактический тип параметра оосв1евепт.
10.3. Уточнение реализации: шаблон Асусйс Ч!аког Итак, вы решили применить шаблон У151тог. Вас интересует, как это сделать в реальном проекте? 2Вг Часть (!. Компоненты Анализ зависимостей между классами в предыдущем примере приводит нас к следующим выводам. ° Для того чтобы скомпилировать определение класса оосе1евепс, необхолимо знать о существовании класса оосе1евепсч)з(сог, поскольку он упоминается в сигнатуре функции-члена оосЕ1евепс: гдссерс. Для этого достаточно сделать неполное обьявление класса ((опчагд дес!ага1юп).
° Для того чтобы скомпилировать определение класса оосе1евепсчз аз сог, необходимо знать о существовании всех конкретных классов, входящих в иерархию каасса оосе1евепс, поскольку имена этих классов встречаются в функцияхчленахч(зтсккхкласса оосе1евепсч(ззсог. Такой тип зависимости называется циклическим (сус!ьз дерепдепсу).
Циклические зависимости создают хорошо известные трудности. Для класса ОосЕ1евепс необходим класс оосЕ1евепсч(зтсог, а классу оосЕ1евепсч1з(сог нужны все классы из иерархии класса оосе1евепс. Следовательно, класс оосе1евепс зависит от всех своих подклассов. Фактически здесь проявляется циклическая зависимасвь па имени (сус!(с паше дерепдепсу), т.е. для компиляции опрелелений классов нужны лишь их имена. Таким образом, классы следует разделить по файлам.
// еайл оосе1евепсч1эдсог.п с1азз оосе1евепс; с1ааа рагайгарп; с!ада яазсегвдсвар; неполные объявления всех подклассов класса оосе1евепс с)ааэ оосе1евепсч1з(сог ( чдгсца1 чоИ чзззсрагаагарЫрагадгарбб) О; И гсца1 чо1д чзз1сяазсегв1свар(яазсегвзсварб) О; ... другие подобные функции // еайл оосе1евепс.'и с!ада оосе1евепсчдз(сог; с1аав ОосЕ1евепС ( риЫзс: ч!гсиа! чодд десерт(юосЕ1евепсчзз1согб) = О; )! Более того, для каждой однострочной реализации функции десерт необходимо знать определение класса оосЕ1евепсч)зтсог, а каждый конкретный инспектор должен быть внедрен в класс, который он должен инспектировать.
Все это порождает весьма запутанную схему взаимозависимостей. Дабаллепиа нооьгк подклассов клхаа расе !атпе ствподт(тстг к/таина слдхнии. г(0 ведь мы и не собирались лобавлять в иерархию класса оосе1евепс никаких новых элементов! Шаблон чдззсог предназначен прежде всего для устойчивых иерархий, в которые добавляются лишь операции, а не новые классы, Стоит напомнить, что шаблон чт аз сог позволяет это сделать за счет усложнения процедуры добавления новых производных классов в инспектируемую иерархию (в нашем примере — в иерархию класса оосЕ1евепт). Однако жизнь — сложная штука. В реальном мире устойчивых иерархий вообще не бывает. Когда-нибудь вам понадобится добавить новый класс в иерархию класса 263 Глава 1О. Шаблон Ч!в!Сог оосе1евепт.
В предыдушем примере для этого придется добавить новый класс честогегарЫс, производный от класса оосе1евепт. Следуйте нашим указаниям. ° Откройте файл оосЕ1евептИззтог.п и добавьте новое неполное объявление класса честогегарЫ с. ° Включите в класс оосе1евепт новую чисто виртуальную перегруженную функцию. с1ааа оосе1евептч!з!тог ( как и прежде чз гтца1 чо!д ч!ззтчестогегарЫ с(честогегарЫсб) = О; ); ° Реализуйте в каждом конкретном инспекторе функцию чзззтчестогегарЫс. Эта функция может быть пустой. В качестве альтернативы можно определить пустую функцию оосе1евептч!ззтог::ч!з1тчесгогегарЫс, однако в этом случае компилятор не сообшит ничего, если вы забудете реализовать ее в конкретных инспекторах.
° Реализуйте функцию десерт в классе честогегарЫс. Сдыайте зто обязательно. Если, применяя прямое наследование от класса оосе1евепт, вы забудете реализовать функцию десерт, ничего страшного не произойдет — вы получите сообшение об ошибке компиляции. Однако, если вы выполните наследование от другого класса, например, класса егарп)с, в котором опрелелена функция десерт, компилятор не проронит ни слова о том, что вы забыли сделать класс честогегарЫ с инспектируемым.
Эта ошибка проявится лишь во время выполнения программы, когда вы с удивлением обнаружите, что ни одна реализация функции ч! з)тчестогпгарЫ с не вызывается. Найти такую ошибку достаточно сложно. Сзедствие; все иерархии классов оосЕ1евепс и оосЕ1евептчзз!тог будут скомпилированы повторно, и вам придется добавлять довольно большое количество механического кода лишь для того, чтобы сохранить работоспособность программы.
В некоторых случаях такое решение может оказаться совершенно неприемлемым. Роберт Мартин ()соЬеп Мап!и, !996) изобрел интересную разновидность шаблона ч)з)тог, в котором цикличность ликвидируется с помошью оператора дупаапс сазт. В рамках этого подхода для иерархии инспектора создается фиктивный базовый класс вазеИ з)тог, выступающий в роли носителя информации. Он не имеет никакого содержания, а следовательно, совершенно независим. Функция-член лссерт из инспектируемой иерархии получает ссылку на объект класса вазеч) з! тог и применяет к нему оператор булав)с сазт., чтобы обнаружить соответствуюший инспектор. Если соответствие достигнуто, функция десерт переходит от инспектируемой иерархии к иерархии инспектора. Это звучит довольно загалочно, но на самом деле все очень просто.
Посмотрим, как можно реализовать шаблон лсус1)с ч) збтог в проекте оосЕ1евепт/ оосе1евептч)з1тог. Во-первых, определим базовый класс инспектора. с1аьа оосе1евептИ з1тог ( ри!з1! с: ч! гама! чо! 6 -оосе1 еже птч! з! Еог О () ); Пустой виртуальный деструктор выполняет две важные веши. Во-первых, он предоставляет в распоряжение класса оосЕ1евептч! з)тог функциональные возможности механизма )(ТТ! (гипбше суре ш(оппайоп). (Оператор булав)с сазт можно применять лишь к тем типам, которые содержат хотя бы одну виртуальную функцию.) Во- 264 Часть й. Компоненты вторых, виртуальный деструктор гарантирует корректное полиморфное уничтожение объектов класса оося1евепсч1з1сог. Полиморфная иерархия без виртуальных деструкторов вызывает непредвиденные последствия, если объект производного класса разрушается через указатель на объект базового класса.