|
Программирование >> Синтаксис инициирования исключений
Чтобы усилить эффект, закрытые классы можно объявить статистическими и тем самым ограничить их пространтсво имен исходным файлом, в котором они находятся. С инкапсулированными производными классами связаны определенные проблемы. Например, как создать экземпляры таких классов, как Dad, которые не видны клиенту из файла .h? Эти проблемы легко решаются с помощью идиом, описанных в двух следующих главах. А пока мы продолжим публиковать производные классы в файле .h, зная, что существует возможность их полной инкапсуляции. Множественная передача Самый распространенный пример гомоморфной иерархии - набор классов, соответствующих различным видам чисел: целым, комплексным, вещественным и т.д. Класс-предок такой иерархии может называться Number и иметь интерфейс следующего вида: class Number { public: virtual Number operator+(const Number&) = 0; virtual Number operator-(const Number&) = 0; И т.д. class Integer : public Number { private: int i; public: Integer(int x) : i(x) {} virtual Number operator+(const Number&); И т.д. На бумаге все выглядит проще, чем в действительности. Как реализовать Integer::operator+(Number&), если нам не известно, что в скобках находится вовсе не Number, а некоторый производный класс? Для каждой пары типов, участвующих в сложении, существует свой алгоритм. Суммирование Complex + Integer отличается от Integer + Real, которое, в свою очередь, отличается от Integer + ArbitraryPrecisionNumber . Как программе разобраться, какой из алгоритмов следует использовать? Что-что? Кто сказал: Запросить у аргумента оператора + его настоящий тип ? Немедленно встаньте в угол. class Number { protected: int type; Хранит информацию о настоящем типе int TypeOf() { return type; } И т.д. Где-то в программе switch (type) { case kInteger: ... case kComplex: ... Именно этого знания типов мы постараемся избежать. Кроме того, все прямые реализации подобных схем не отличаются особой элегантностью. Вы когда-нибудь видели код, генерируемый компилятором для конструкции switch/case? Ни красоты, ни эффективности. Вместо этого мы объединим знания компилятора о типах с чудесами современной технологии - v-таблицами. Двойная передача В обобщенном виде задачу можно представить в виде матрицы, строки которой соответствуют типам левого операнда, а столбцы - всевозможным типам правого операнда. В каждой ячейке матрицы находится конкретный алгоритм для обработки сочетания типов. Чаще всего такая ситуация возникает для гомоморфных иерархий вроде нашей, но вообще типы левого операнда не обязаны совпадать с типами правого операнда. Конечно, возможны силовые решения - например, запрятать в каждом экземпляре сведения о его типе. Однако более элегантное решение (и обычно более эффективное) решение носит название двойной передачи (double dispatch). class Number { protected: Диспетчерские функции для оператора + virtual Number& operator+(const Integer&) = 0; virtual Number& operator+(const Comp1et&) = 0; И т.д. для всех производных типов public: virtual Number& operator+(const Number&) = 0; virtual Number& operator-(const Number&) = 0; И т.д. class Integer : public Number { private: int I; protected: virtual Number& operator+(const Integer&); virtual Number& operator+(const Comp1ex&); public Integer(int x) : i(x) {} virtual Number& operator+(const Number&); И т.д. Number& Integer::operator+(const Number& n) return n + *this; Поменять местами левый и правый операнд Number& Integer::operator+(const Integer& n) Ниже приведен псевдокод if (i + n.i слишком велико для int) { return ЦелоеСПовышеннойТочностью else return Integer(i + n.i); С этим фрагментом связана одна нетривиальная проблема, к которой мы вернемся позже, а пока сосредоточьте все внимание на концепции. Она похожа на стереограмму - чтобы скрытая картинка проявилась, вам придется расслабить глаза и некоторое время рассматривать код. Когда клиент пытается сложить два Integer, компилятор передает вызов Integer::operator+() , поскольку operator+(Number&) является виртуальным - компилятор правильно находит реализацию производного класса. К моменту выполнения Integer::operator+(Number&) настоящий тип левого операнда уже известен, однако правый операнд все еще остается загадкой. Но в этот момент наступает второй этап двойной передачи: return n + *this. Левый и правый операнды меняются местами, а компилятор приступает к поискам v-таблицы n. Однако на этот раз он ищет переопределение Number::operator+(Integer&), так как он знает, что *this в действительности имеет тип Integer. Это приводит к вызову Integer ::operator+(Integer&), поскольку типы обоих операндов известны и можно наконец произвести вычисления. Если вы так и не поняли, что же происходит, прогуляйтесь на свежем воздухе и попробуйте снова, пока не поймете. Возможно, вам поможет следующая формулировка: вместо кодирования типа в целой переменной мы определили настоящий тип Number с помощью v-таблицы. Такое решение не только элегантно. Вероятно, оно еще и более эффективно, чем те, которые приходили вам в голову. Скажем, приходилось ли вам видеть код, генерируемый компилятором для конструкции switch/case? Он некрасив и вдобавок куда менее эффективен, чем последовательное индексирование двух v-таблиц. Несмотря на всю элегантность, двойная передача довольно дорого обходится по объему кода и сложности: Если у вас имеется m производных классов и n операторов, то каждый производный класс должен содержать m*(n+1) виртуальных функций, да еще столько же чисто виртуальных заглушек в классе-предке. Итого мы получаем (m+1)*m*(n+1) диспетчерских функций. Для всех иерархий, кроме самых тривиальных, это довольно много. Если оператор не является коммутируемым (то есть ему нельзя передать повторный вызов с аргументами, переставленными в обратном порядке), это число удваивается, поскольку вам придется реализовать отдельные функции для двух вариантов порядка аргументов. Например, y/x - совсем не то же, что x/y; вам понадобится оператор / и специальная функция DivideInto для переставленных аргументов. Клиенты базового класса видят все устрашающие защищенные функции, хотя это им совершенно не нужно. Тем не менее, в простых ситуациях двойная передача оказывается вполне разумным решением - ведь проблема, как ни крути, достаточно сложна. Специфика ситуации неизбежно требует множества мелких фрагментов кода. Двойная передача всего лишь заменяет большие, уродливые, немодульные конструкции switch/case более быстрой и модульной виртуальной диспетчеризацией. Как правило, количество функций удается сократить, но при этом приходится в той или иной степени идти на компромисс с нашим строгим правилом - никогда не спрашивать у объекта, каков его настоящий тип. Некоторые из этих приемов рассматриваются ниже. Видимость производных классов для клиентов Number тоже удается ликвидировать минимальной ценой; об этом будет рассказано в главе 12. Как и во многих проблемах дизайна в С++, в которых задействованы матрицы операций, вам придется на уровне здравого смысла решить, стоит ли повышать модульность за счет быстродействия или объема кода. Гетероморфная двойная передача Двойная передача обычно возникает в ситуациях, когда оба аргумента происходят от общего предка, но это не обязательно. Левый и правый операнды могут принадлежать к разным классам, не имеющим общего предка. Один из моих любимых примеров относится к обработке событий в графических средах. Существует множество возможных событий: операции и мышью, события от клавиатуры, события операционной системы и даже такая экзотика, как распознавание голоса или световое перо. С другой стороны, в пользовательский интерфейс обычно входят разнообразные виды, панели или окна (терминология зависит от операционной системы и используемого языка) - внешние окна с заголовками и кнопками закрытия, поля для редактирования текста и области, в которых можно рисовать красивые картинки. Для каждой комбинации конкретного события с конкретным типом вида может потребоваться уникальная реализация. Возникает та же проблема, что и с иерархией чисел, хотя на этот раз события и виды не имеют общего базового класса. Тем не менее, методика двойной передачи все равно работает. class Event { Чисто виртуальный базовый класс для событий public:
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |