Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Элджер Д. - C++. Библиотека программиста - 1999.pdf
Скачиваний:
141
Добавлен:
13.08.2013
Размер:
1.98 Mб
Скачать

162

Чтобы усилить эффект, закрытые классы можно объявить статистическими и тем самым ограничить их пространтсво имен исходным файлом, в котором они находятся.

С инкапсулированными производными классами связаны определенные проблемы. Например, как создать экземпляры таких классов, как 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-таблицами.

163

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

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

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

двойной передачи (double dispatch).

class Number { protected:

//Диспетчерские функции для оператора + virtual Number& operator+(const Integer&) = 0; virtual Number& operator+(const Complet&) = 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 Complex&);

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&) настоящий тип левого

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:

165

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(Complex& n1, Number& n3) = 0;

//И т.д. для всех типов в первой позиции

//‘this’ – неявный третий аргумент

virtual Number& fn2(Integer& n1, Integer& n2) = 0; virtual Number& fn2(Integer& n1, Complex& n2) = 0; virtual Number& fn2(Complex& n1, Integer& n2) = 0; virtual Number& fn2(Complex& n1, Complex& 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(Complex& n1, Number& n3)

Соседние файлы в предмете Программирование на C++