|
Программирование >> Оптимизация возвращаемого значения
public: virtual void collide(GameObjectSc otherObject) virtual void collide(SpaceShipSc otherObject) virtual void collide(SpaceStationSc otherObject) virtual void collide(AsteroidSc otherobject) Основная идея заключается в том, чтобы реализовать двойную диспетчеризацию как две одиночных, то есть как два отдельных вызова виртуальных функций; первый определяет динамический тип первого объекта, а второй - второго объекта. Как и в предыдущем примере, вначале вызывается виртуальная функция collide с параметром GameObj ect&. Код этой функции в данном случае поразительно прост: void Spaceship::collide(GameObjectSc otherObject) { otherObject.collide(*this); Ha первый взгляд кажется, что это не более чем рекурсивный вызов функции collide с параметрами, заданными в обратном порядке, то есть что OtherObject становится объектом, вызывающим функцию - член класса, а объект *this - параметром функции. Но взгляните на код еще раз и вы поймете: это не рекурсивный вызов. Как известно, компиляторы определяют, какая из набора функций должна быть вызвана, основываясь на статическом типе передаваемых функции аргументов. В этом случае могут быть вызваны четыре различные функции collide, но из них выбирается одна на основе статического типа *this. Что это за статический тип? Так как вы находитесь внутри функции - члена класса Spaceship, то объект *this должен иметь тип Spaceship. Поэтому вызывается функция collide, принимающая параметр Spaceships, а не GameObject&. Все функции collide являются виртуальными, следовательно, при вызове функции Spaceship: : collide выполняется подстановка вызова функции, соответствующей настоящему типу объекта otherObj ect. Внутри этой реализации функции collide известен настоящий тип обоих объектов, так как объект слева - это * thi S (и поэтому имеет тип класса, в котором реализована данная функция), а объект справа имеет тип, объявленный как тип параметра. Все это станет для вас более ясным, когда вы увидите реализацию других функций collide в классе Spaceship: void Spaceship: : collide (SpaceShipSc otherObject) ( обработка столкновения Spaceship-Spaceship; void Spaceship::collide(SpaceStationSc otherObject) { обработка столкновения SpaceShip-SpaceStation; void Spaceship::collide(Asteroids otherObject) { обработка столкновения SpaceShip-Asteroid; Как видите, здесь нет ни RTTI, ни необходимости генерировать исключения для неизвестных типов объектов. Неизвестных типов объектов просто не может быть - в этом и состоит особенность использования виртуальных функций. И если бы ни одна неисправимая ошибка, это было бы идеальным решением проблемы двойной диспетчеризации. Такая ошибка присуща и рассмотренному ранее подходу RTTI: каждый из классов должен знать обо всех родственных классах. Код должен обновляться по мере добавления новых классов. Однако способ обновления кода в данном случае другой. Не нужно модифицировать цепочки if-then-else, но есть нечто гораздо худшее: в каждое определение класса должна быть внесена новая виртуальная функция. Если, например, вы решите включить в код игры новый класс Satellite (наследующий от класса GameObject), то вам придется добавить новую функцию collide к каждому из существующих классов в программе. Во многих сл5Д1аях изменить существующие классы нельзя. Если вместо того, чтобы писать всю видеоигру целиком, вы начали с готовой библиотеки, определяющей прикладной интерфейс видеоигры, у вас может не быть доступа на запись к классу GameObject или производным от него классам. При этом невозможно добавление новых функций, независимо от того, виртуальные они или нет. Или классы, требующие изменения, могут быть доступны физически, но не практически. Предположим, вы были-таки приняты на работу в корпорацию Nintendo и начали работу над программами, которые используют библиотеку, содержащую класс GameObject и другие полезные классы. Несомненно, вы не будете единственным клиентом библиотеки, и руководство наверняка не будет слишком радо тому, что при каждом добавлении нового типа к вашей программе будут перекомпилироваться все приложения, работающие с этой библиотекой. Обычно часто используемые библиотеки изменяются очень редко, так как стоимость перекомпиляции всего основанного на них кода слишком велика. Короче говоря, если вам необходимо реализовать двойную диспетчеризацию в вашей программе, прежде всего попытайтесь изменить концепцию программы, чтобы избежать перекомпиляции. Если же это невозможно, то подход с использованием виртуальных функций является более безопасным, чем стратегия RTTI. Последняя ограничивает расширяемость системы, которая определяется возможностью редактировать заголовочные файлы, но, с другой стороны, не требует перекомпиляции. Однако, если методика RTTI реализована так, как показано выше, это обычно приводит к созданию программ, которые сложно поддерживать. Вы платите, и вы делаете выбор. Эмуляция таблиц виртуальных функций Ваши шансы на успех можно увеличить. Вспомните, в правиле 24 говорилось о том, что компиляторы обычно реализуют виртуальные функции, создавая void Spaceship::hitSpaceShip(SpaceShipSc otherObject) обработать столкновение SpaceShip-SpaceShip; void Spaceship::hitSpaceStation(SpaceStationSc otherObject) обработать столкновение SpaceShip-SpaceStation; void Spaceship::hitAsteroid(AsteroidSc otherObject) обработать столкновение SpaceShip-Asteroid; Так же как и в RTTI-иерархии, рассмотренной выше, класс GameObj ect содержит только одну функцию для обработки столкновений, которая выполняет первую диспетчеризацию (из двух необходимых). Так же как и в иерархии, основанной на виртуальных функциях, здесь каждый тип столкновений инкапсулирован в отдельную функцию, хотя в этом cnjae функции имеют различные имена, а не одно и то же - collide. Для того чтобы отказаться от перегрузки, есть свои причины, и вскоре они будут разъяснены. Пока же заметим, что вышеприведенная схема содержит все необходимое, кроме реализации функции Spaceship: : coll ide; именно в ней будут вызываться различные функции hit. Как и раньше, после массив указателей на функции (виртуальные таблицы) и затем выполняя индексирование в этом массиве при вызове виртуальной функции. Использование виртуальной таблицы устраняет необходимость выполнять цепочки if-then-else и позволяет компилятором генерировать один и тот же код для всех вызовов виртуальных функций: определить правильный индекс в виртуальной таблице, а затем держа, вызвать функцию, указанную этим положением в ней. В результате ваш RTTI-код станет более эффективным (индексирование в массиве и вызов функции по указателю почти всегда эффективнее, чем выполнение серии проверок if-then-else, и дает более компактный код), кроме этого, RTTI используется только в одном месте: там, где инициализируется массив указателей на функции. Итак, внесем несколько изменений в функции в иерархии GameObject: class GameObject { public: virtual void collide(GameObjectSc otherObject) = 0; class Spaceship: public GameObject { public: virtual void collide(GameObjectSc otherObject); virtual void hitSpaceShip(SpaceShipSc otherObject); virtual void hitSpaceStation(SpaceStationSc otherObject); virtual void hitAsteroid(AsteroidSc otherobject);
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |