Анимация
JavaScript
|
Главная Библионтека Двойная передача В обобщенном виде задачу можно представить в виде матрицы, строки которой соответствуют типам левого операнда, а столбцы - всевозможным типам правого операнда. В каждой ячейке матрицы находится конкретный алгоритм для обработки сочетания типов. Чаще всего такая ситуация возникает для гомоморфных иерархий вроде нашей, но вообще типы левого операнда не обязаны совпадать с типами правого операнда. Конечно, возможны силовые решения - например, запрятать в каждом экземпляре сведения о его типе. Однако более элегантное решение (и обычно более эффективное) решение носит название двойной передачи (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: virtual void Process(View* v) = 0; class MouseClick : public Event { public: virtual void Process(View* v) { v->Process(*this); } class View { Чисто виртуальный базовый класс для видов public: virtual void Process(MouseClick& ev) = 0; virtual void Process(Keystroke& ev) = 0; И т.д. Хотя на первый взгляд кажется, что проблема отличается от иерархии Number, присмотритесь повнимательнее. Реализация функции Process() класса Event всего лишь «разворачивает» операцию и перенаправляет вызов. Поскольку функция Event::Process() является виртуальной, когда дело доходит до класса View, точный тип Event уже известен, и компилятор вызывает правильную перегруженную версию View::Process(). Каждый раз, когда вам захочется забить код типа в переменную класса, чтобы узнать, с каким производным классом вы имеете дело, подумайте, нельзя ли переложить хлопоты на компилятор с помощью двойной передачи (или одного из описанных ниже вариантов). Передача более высокого порядка До сих пор мы рассматривали только бинарные функции, однако та же методика распространяется на функции с производным количеством аргументов неизвестного типа. Если функция имеет n аргументов, передавать придется n раз. Предположим, у вас имеется функция, которая получает три аргумента Number и возвращает Number&. Можно устроить, чтобы первый аргумент оказался в левой части оператора -> (или .), а дальше начинаются комбинаторные игры. class Number { protected: this - неявный второй аргумент virtual Number& fn1(Integer& n1, Number& n3) = 0; virtual Number& fn1(Comp1ex& n1, Number& n3) = 0; И т.д. для всех типов в первой позиции this - неявный третий аргумент virtual Number& fn2(Integer& n1, Integer& n2) = 0; virtual Number& fn2(Integer& n1, Comp1ex& n2) = 0; virtual Number& fn2(Comp1ex& n1, Integer& n2) = 0; virtual Number& fn2(Comp1ex& n1, Comp1ex& n2) = 0; И т.д. для всех сочетаний public: this - неявный первый аргумент virtual Number& fn(Number& n2, Number& n3) = 0; class Integer : public Number { protected: this - неявный второй аргумент virtual Number& fn1(Integer& n1, Number& n3) { return n3.fn2(n1, *this); } virtual Number& fn1(Comp1ex& n1, Number& n3) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 [ 50 ] 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |