А. Александреску - Современное проектирование на C++ (1119444), страница 68
Текст из файла (страница 68)
Овладев этой методикой, вы сможете самостоятельно распространить ее ' Если вам не удастся реализовать мультиметоды, рассматривайте зту главу в качестве снотворного средства, хотя я надеюсь, что она не обладает усыпляющим эффектам, на любое количество объектов. Однако в большинстве ситуаций можно обойтиаь двойной диспетчеризацией, непосредственно воспользовавшись библиотекой 1л)о.
11.1. Что такое мультиметоды? В языке С++ полиморфизм, по существу, означает, что вызов данной функции связан с разными реализациями и зависит ат статического нли динамического контекста. В языке С++ реализованы два типа полиморфизма. ° Статический полиморфизм (сагир!1е-г!ще ро1угпогрЬ!зш), осуществляемый с помощью перегрузки и шаблонных функций.' ° Динамический полиморфизм (пзп-г!пзе ро!углогрй!зш)м, реализуемый виртуальными функциями. Перегрузка — это простой вид полиморфизма, позволяющий нескольким одноименным функциям существовать в одной и той же области видимости. Если эти функции имеют разные списки параметров, компилятор может различать их во время компиляции.
Перегрузка представляет собой удобную синтаксическую конструкцию, позволяющую сократить текст программы. Шаблонные функции являются основой статического механизма диспетчеризации. Они осуществляют более сложный статический полиморфизм. Имя виртуальной функции связывается с ее конкретной реализацией во время выполнения программы в зависимости от динамического типа объекта, к которому она применяется.
Посмотрим, как масштабируются эти три вида полиморфизма лля нескольких объектов. Для перегруженных и шаблонных функций это происходит вполне естественно. Оба механизма допускают использование нескольких параметров, а выбор функции производится на основе запутанных статических правил. К сожалению, виртуальные функции — единственный механизм, реализующий динамический полиморфизм в языке С++, — могут работать только с одним объектоль Даже синтаксис вызова виртуальной функции — оЬ). яип(аргументы) — отдает предпочтение объекту оЬ! перед аргументами. (Фактически объект оЬ) является лишь одним из аргументов функции гип, доступ к которому осуществляется с помощью указателя *тЬ)з. В языке Оу!ап, например, оператор "точка" является лишь одним из многих конкретных воплощений общего механизма вызова функции.) Мы будем называть мультиметадамн, или множественной диспетчеризанией, механизм, связывающий вызов с разными конкретными функциями в зависимости от динамических типов нескольких объектов, участвующих в вызове.
Поскольку статический мультиобъектный полиморфизм уже существует, остается только реализовать его динамический аналог. 11.2. Когда нужны мультиметоды Опрелелить, котла следует использовать мультиметоды, очень просто. Допустим, в программе есть операция, манипулирующая несколькими полиморфными объектами З Автамап1чеакае преобразование типов можно было бы квалифицировать как простейший вил палимарфизма, паахатьху ана, например, позволяет вызывать функцию з1п:: з1п а параметрам, имаюшим тип з па, хотя изначально параметр этой функции имеет тип боиЬ)е. Однако эта мнимый полиморфизм, поскольку к пархчалрам обоих типов применяется одна и та 'ке функция. 282 Часть й.
Компоненты с помощью указателей или ссылок на их базовые классы. Необходимо модифицировать операцию в соответствии с линамическими типами этих объектов. Столкновения (со11гаюпз) представляют собой типичную категорию проблем. которые лучше всего решать с помощью мультиметодов. Предположим, что мы разрабатываем видеоигру, в которой движущиеся объекты выводятся из абстрактного класса савеОЬ)ест. Нам хотелось бы, чтобы их столкновение зависело от типов сталкиваю- шихся обьектов: космического корабля с астероидом, корабля с космической станцией или астероида со станцией.' Допустим, что мы хотим выделять области перекрытия двух нарисованных обьектов. Напишем программу для рисования, позволяющую пользователю определять прямоугольники, круги, эллипсы, многоугольники и другие фигуры.
В основу разработки положим классический объектно-ориентированный подход; определим абстрактный класс з(заре и выведем все конкретные фигуры из него. Затем будем управлять рисованием с помощью набора указателей на объекты, производных от класса взваре. Туг к нам подходит клиент и просит предусмотреть следующее: если две фигуры пересекутся, область их пересечения нужно нарисовать иначе, чем сами фигуры, например, заштриховать (рис. 11.1). Рис.
П, б Штриховка пересечении двух фигур Разработать один алгоритм, заштриховывающий любое пересечение, сложно. Например, алгоритм лля штриховки пересечения эллипса и прямоугольника намного сложнее, чем алгоритм для штриховки пересечения двух прямоугольников. Кроме того, чрезмерно общий алгоритм (например, на уровне работы с пикселями) будет слишком неэффективен, поскольку некоторые пересечения (скажем, двух прямоугольников) тривиальны. Здесь нужен набор алгоритмов, специализированных лля каждой пары фигур, например, прямоугольник-прямоугольник, прямоугольник-многоугольник, многоугольник-многоугольник, эллипс-многоугольник и эллипс-эллипс.
В ходе выполнения программы, когда пользователь перемешает фигуры по экрану, нужно выбрать правильный алгоритм„который бы закрашивал пересечение фигур достаточно быстро. Поскольку манипуляции фигурами осуществляются с помощью указателей на класс БЬаре, у нас нет информации о типах объектов, позволяющей выбирать нужный алгоритм. Придется работать только с указателями на класс збаре. Поскольку в каждом пересечении участвуют два объекта, простые виртуальные функции не решат нашу задачу. Следует применить двойную диспетчеризацию. з этот пример и имена позаимствованы из книги скотта мейерса "маге ейссгые с++*' (1996а), задача 31.
Глава 11. Мультиметоды 283 11.3. Двойное переключение по типу: грубый подход Проше всего реализовать двойную диспетчеризацию на основе двойного переключения по типу (доцЫе ззчйсп-оп-гуре). Для этого нужно попытаться последовательно выполнить динамическое приведение типа первого объекта к каждому из типов объектов, которые могут стоять в левой части выражения. Для каждой ветви нужно сделать то же самое со вторым аргументом. После обнаружения типов для обоих объектов становится ясно, какую функцию следует вызвать. // Разные алгоритмы закраски пересечения фигур чоз'гз ронатсплгеа2(яестапд1ей, яестапд1ей); чеза ронатсйагеа2(яестап91ей, е111рзей); чей оонатспдгеаЗ(яестапд1ей„ Ро1уй); чеза оонатспдгеа4(е111рзей, Ро1уй); чей оонатсйдгеа5(е11зрзей, е11з раей); чозг) оонассйдгеаб(Ро1уй, Ро1уй); чозг) ОоиЫерззратсн(5йарей 1нз, Ейарей гйз) ( И (яестап91е* р1 дупаюс сазт<яестапд)е*>(й)йз)) ( и (яестапд1е* р2 = дупаюс сазт<яестап91е*>(йгнз)) оонатсйдгеа1(*рз, *р2); е1зе зЕ (е11зрзе р2 = дупавзс сазт<е115рзе*>(йгйз)) Оонатсндгеа2(*р1, *р2); е1зе зб (Ро1у р2 = ггупавзс сазт<Ро1у*>(йгнз)) оонатснлгеаз(>р1, *р2); е1зе еггог("неопределенное пересечение"); е1зе Зб (е11зрзе* р1 = дупавзс сазт<е11зрзе*>(й)нз)) ( зЕ (яестап91е* р2 = дупавзс сазт<яестапд1е*>(йгнз)) оонатсйдгеа2 (*р2 „*р1); е1зе з1 (е11зрзе р2 дупавзс сазт<е114рзе*>(йгнз)) оонатсндгеа5(+р1, <р2); е1зе з1 (Ро1у р2 = оупавзс сазт<Ро1у*>(йгйз)) оонатсндгеа4(*р1, "р2); е1зе еггоггнеопределенное пересечение )„' е1зе И (Ро1у" р1 дупаюс сазт<Ро1у*)(й)йз)) зУ (яестап91е* р2 оупавзс сазт<яессап91е">(йгпз)) оонатсндгеа2(*р2, 'р1); е1зе И (Е114рзе р2 = оупавтс сазт<Е11з рве*>(йгйз)) оонатсйлгеа4(*р2, *р1); е1зе зР (Ро1у р2 дупавзс сазт<Ро1у*>(йгпз)) оонатсйлгеаб(*р1, *р2); е1зе Еггог( Неопределенное пересечение"); ) е1зе еггог("неопределенное пересечение"); 284 Часть й.
Компоненты Надо же! Оказалось, достаточно нескольких строк, Очевидно, что подобный грубый подход вынуждает нас писать большое количество внешне тривиальных кодов. Такую паутину нз условных операторов может сплести любой программист. Кроме того, это решение оказывается достаточно эффективным, если количество классов не слишком велико. Функция ооиЫео1зрассп выполняет линейный поиск во множестве возможных типов. Поскольку поиск разворачивается (программа содержит последовательность операторов 1з-е1зе, а не цикл), для небольших множеств алгоритм будет работать достаточно быстро. Грубый подход порождает неприемлемый размер программы. При большом количестве классов программу становится невозможно поддерживать.