Анимация
JavaScript


Главная  Библионтека 

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

Двойная передача

В обобщенном виде задачу можно представить в виде матрицы, строки которой соответствуют типам левого операнда, а столбцы - всевозможным типам правого операнда. В каждой ячейке матрицы находится конкретный алгоритм для обработки сочетания типов. Чаще всего такая ситуация возникает для гомоморфных иерархий вроде нашей, но вообще типы левого операнда не обязаны совпадать с типами правого операнда.

Конечно, возможны силовые решения - например, запрятать в каждом экземпляре сведения о его типе. Однако более элегантное решение (и обычно более эффективное) решение носит название

двойной передачи (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