Есть очень нетривиальный способ реализовать двойное переключение по типу с использованием только виртуальных функций. Как обычно, сначала реализуем мультиметоды для двух классов (листинг 10.10), а затем попытаемся добавить третий. Основная идея состоит в том, чтобы реализовать двойную диспетчеризацию как две одиночные, выполняемые последовательно; первый виртуальный вызов определяет динамический тип первого объекта, а второй — второго объекта.
Листинг 10.10. Мультиметоды — только виртуальные функции
// предварительное объявление наследников class Derived_a; class Derived_b; class Derived_c; // абстрактный базовый класс class Base { public: virtual ~Base()=0;
virtual bool Operator(const Base &R)const = 0; virtual bool Operator(const Derived_a &R)const = 0; virtual bool Operator(const Derived_b &R)const = 0;
};
Base::~Base(){}
// определение классов-наследников class Derived_a: public Base { public:
virtual bool 0perator(const Base &R) const
{ return R.Operator(*this); }; // перенаправление виртуальности virtual bool Operator(const Derived_a &R)const { cout << "A-A" << endl; return true; } virtual bool Operator(const Derived_b &R)const { cout << "A-B" << endl; return true; }
};
class Derived_b: public Base { public:
virtual bool Operator(const Base &R)const
{ return R.Operator(*this); }; // перенаправление виртуальности virtual bool Operator(const Derived_a &R)const { cout << "B-A" << endl; return true; } virtual bool Operator (const Derived_b &R)const { cout << "B-B" << endl; return true; }
};
Обратите внимание на реализацию первой функции, которая абсолютно одинакова в обоих классах, — именно она реализует двойное переключение по типу:
virtual bool Operator(const Base &R) const { return R.Operator(*this); };
Чтобы было понятно, где и как она работает, рассмотрим фрагмент программы:
Derived_a A; Derived__b В; // нет необходимости в виртуальности
cout << A.Operator(А) << endl;
cout << A.Operator(В) << endl;
cout << В.Operator(А) << endl;
cout << В.Operator(В) << endl;
Base &D = A;
cout << A.Operator(D) << endl; // работают 2 виртуальных вызова
cout << В.Operator(D) << endl; // работают 2 виртуальных вызова
cout << D.Operator(D) << endl; cout << D.Operator(A) << endl; cout << D.Operator(B) << endl;
// работают 2 виртуальных вызова // работает 1 виртуальный вызов // работает 1 виртуальный вызов
В первых четырех вызовах нет необходимости в двойной диспетчеризации, так как нет подстановки ссылки на наследника вместо ссылки на базовый класс — вызываются сразу нужные функции. Эти функции и выводят на экран строки, соответствующие типам операндов:
А-А А-В В-А В-В
Следующие три вызова как раз и выполняют двойную диспетчеризацию. В первом варианте мы сначала попадаем в первый метод класса Derived_a, так как тип левого аргумента именно этот. При этом на место параметра-ссылки R подставляется фактический аргумент-ссылка D, инициализированный ссылкой А типа Deri ved__a, а *thi s внутри метода статически имеет тип Deri ved_a. Таким образом, вызов превращается в A. Operator (А), и на экран выводится строка «А-А». Второй вариант вызова сначала приводит нас к первой функции класса В, так как левый аргумент вызова имеет тип Deri ved_b. Снова на место ссылки R подставляется ссылка D, инициализированная ссылкой типа Derived_a. Однако мы находимся внутри класса Derived_b, поэтому * thi s имеет тип Derived_b и вызов превращается в A.Operator(B), что мы и наблюдаем при выводе на экран строки «А-В». В третьем случае сначала виртуализируется левый аргумент, и вызов направляется в класс Der i ved_a, а дальше процесс продолжается по первому варианту, и на экран выводится строка «А-А».
Четвертый виртуальный вызов превращается в А. Operator (А), а пятый — в А. Operator (В). Виртуальность работает только один раз — для левого аргумента.
Теперь проделаем то же самое с указателями:
Base *рА = new Derived_a(); Base *рВ = new Derived_b(); cout << pA->Operator(*pA) << endl; cout << pA->Operator(*pB) << endl; cout << pB->Operator(*pA) << endl; cout << pB->Operator(*pB) << endl;
Первый вариант, естественно, сначала приводит нас в первый метод класса De-r i ved_a, который вызывает второй метод того же класса с аргументом того же типа. Второй вызов тоже сначала сопоставляется с первым методом класса De-rived_a, но поскольку в этом случае на место R поставляется ссылка на объект типа Deri ved_b, a *thi s имеет тип Deri ved_a, то вызывается метод с прототипом
virtual bool Operator(const Derived_a &R) const;
На экране, естественно, получаем строку «В-А». Третий и четвертые варианты, естественно, первый вызов осуществляют из класса Deri ved_b, а второй — в соответствии с аргументом * thi s.
Таким образом, методы с параметром типа Base осуществляют то самое двойное переключение по типу.
Этот способ реализации мультиметодов имеет довольно значительные преимущества по сравнению с предыдущим. Во-первых, нет необходимости генерировать исключение при неправильных типах аргументов, поскольку все проверки осуществляет компилятор. Во-вторых, обратите внимание на значительное упрощение текста методов — реализуется только фактически необходимая работа без всяких лишних проверок. |