164
операнда уже известен, однако правый операнд все еще остается загадкой. Но в этот момент наступает
второй этап двойной передачи:
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: