Недостатки типичной реализации
В статье намеренно не приведен пример типичной реализации паттерна посетителя в C++.
Если вы не знакомы с этим шаблоном, то вот тут можно с ним ознакомиться.
А если в двух словах, то этот шаблон очень полезен если вам нужно обойти коллекцию из указателей на абстрактный базовый класс, применив к ним какую то операцию в зависимости от типа, который скрывается за абстракцией.
Поэтому перейдем сразу к недостаткам, которые хотели бы устранить.
-
При добавлении класса в иерархию (или удалении), приходится изменить много мест в коде. (Каждый раз лезть и добавлять чисто виртуальную функцию-член в абстрактный класс, и почти во все реализации.)
-
При добавлении нового посетителя, приходится наследоваться от абстрактного класса, даже если мы всего то хотим вызвать просто шаблон функции. При этом мы не можем сделать шаблон виртуальной функции-члена visit у посетителя (причина этого проста, шаблонов виртуальных функций-членов просто нет). Каждую реализацию функции-члена visit приходится делать отдельно.
-
Класс посетителя привязан к предметной области.
Что хотим получить?
-
Имея на руках указатель на абстрактный базовый класс, хотим сразу получать реальный тип объекта и отправлять его самого или его тип в какой-нибудь шаблон функции, не используя при этом никаких конструкций из dynamic_cast или static_cast и не создавая множество одинаковых переопределений виртуальных функций.
-
Простым способом добавлять или удалять классы, которые посетитель может обойти.
-
Не привязанный к предметной области посетитель.
Реализация
Начинаем с создания абстрактного посетителя.
Source:
template< class T >struct AbstractVisitor{ virtual ~AbstractVisitor() = default; virtual void visit( T& ) = 0;};
(Пояснение: здесь виртуальная функция-член visit не является шаблоном, количество виртуальных функций при инстанцировании класса AbstractVisitor точно известно т.к. T является параметром шаблона класса, а не функции )
Инстанс конкретного посетителя сможет обойти только один тип объекта. Нам же необходимо чтобы посетитель мог обходить несколько различных типов объектов.
Для этого создадим простой список типов TypeList и класс агрегатор AbstractVisitors. Список у AbstractVisitors будет содержать все типы объектов, которые посетитель может обойти.
Source:
template< class ... T >struct TypeList{};template< class T >struct AbstractVisitor{ virtual ~AbstractVisitor() = default; virtual void visit( T& ) = 0;};template< class ...T >struct AbstractVisitors;template< class ... T >struct AbstractVisitors< TypeList< T... > > : AbstractVisitor< T >...{};
Т.к. мы не хотим каждый раз наследоваться от абстрактного посетителя, наследуемся от него один раз и будем принимать функтор (если быть точным, то принимать будем обобщённую лямбду). Для этого создадим класс Dispatcher.
Source:
template< class Functor, class ... T >struct Dispatcher;template< class Functor, class ... T >struct Dispatcher< Functor, TypeList< T... > > : AbstractVisitors< TypeList< T... > >{ Dispatcher( Functor functor ) : functor( functor ) {} Functor functor;};
Теперь необходимо для всех типов из листа переопределить виртуальную функцию-член visit.
Для этого создадим класс Resolver, который этим и будет заниматься. А сам класс Dispatcher унаследуем от всех возможных типов Resolver-ов.
Дополнительно необходимо вызывать функтор в переопределенной функции, воспользуемся (CRTP) и передадим тип Dispatcher как аргумент шаблона во все Resolver.
(Подробнее о том что такое CRTP можно почитать тут).
Source:
template< class Dispatcher, class T >struct Resolver : AbstractVisitor< T >{ void visit( T& obj ) override { static_cast< Dispatcher* >( this )->functor( obj ); };};template< class Functor, class ... T >struct Dispatcher< Functor, TypeList< T... > > : AbstractVisitors< TypeList< T... > >, Resolver< Dispatcher< Functor, TypeList< T... > >, T >...{ Dispatcher( Functor functor ) : functor( functor ) {} Functor functor;};
Вроде все в порядке.
Попробуем создать объект класс Dispatcher. Компилятор начинает ругаться на то, что объект класса Dispatcher абстрактный, как же так?
Причина этого в том, что мы переопределили виртуальные функции для Resolver, но для Dispatcher мы ведь ничего не переопределяли.
Чтобы этого избежать, необходимо сделать наследование от AbstractVisitor<T>виртуальным.(Подробнее о размещении объектов в памяти и виртуальном наследовании можно почитать тут.)
Source:
template< class ... T >struct AbstractVisitors< TypeList< T... > > : virtual AbstractVisitor< T >...{};template< class Dispatcher, class T >struct Resolver : virtual AbstractVisitor< T >{ void visit( T& obj ) override { static_cast< Dispatcher* >( this )->functor( obj ); };};
Создадим абстрактный базовый класс (AbstractObject) и какие-нибудь классы (Object1, Object2), которые хотели обойти.
Так же создадим функцию test и шаблон функции test, которые будут получать непосредственно ссылку на объект определенного типа.
Пример использования:
Source:
struct Object1;struct Object2;using ObjectList = TypeList< Object1, Object2 >;struct AbstractObject{ virtual void accept( AbstractVisitors< ObjectList >& visitor ) = 0; };struct Object1 : AbstractObject{ void accept( AbstractVisitors< ObjectList >& visitor ) override { static_cast< AbstractVisitor< Object1 >& >( visitor ).visit( *this ); };};struct Object2 : AbstractObject{ void accept( AbstractVisitors< ObjectList >& visitor ) override { static_cast< AbstractVisitor< Object2 >& >( visitor ).visit( *this ); };};void test( Object1& obj ){ std::cout << "1" << std::endl;}template< class T >void test( T& obj ){ std::cout << "2" << std::endl;}int main(){ Object1 t1,t2,t3,t4; Object2 e1,e2,e3; std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 }; auto l = []( auto& obj ){ test(obj); }; Dispatcher<decltype(l), ObjectList> dispatcher; for( auto* obj : vector ) { obj->accept( dispatcher ); }}
(Пояснение: мы не можем просто написать
visitor.visit( *this )
, это приведет к
неоднозначности, если классов в иерархии будет больше
двух.)
Строчки на которых создается обобщенная лямбда и объект класса Dispatchable какие-то то страшные и не удобные, спрятать бы все это от глаз.
Так же, хотелось бы спрятать функцию-член accept у AbstractObject, Object1 и Object2, т.к. тело функции для всех типов объектов будет одинаковое, различаться будет только тип объекта.
Для этого создадим абстрактный класс Dispatchable. Cделаем у него чисто виртуальную функцию-член accept и шаблон функции-члена который будет принимать функтор. В нем собственно и будем создавать наш Dispatcher.
Помимо этого создадим макрос DISPATCHED, он понадобится чтобы спрятать переопределение функции-члена accept у Object1 и Object2.
Source:
template< class TypeList >struct Dispatchable{ virtual ~Dispatchable() = default; virtual void accept( AbstractVisitors< TypeList >& ) = 0; template< class Functor > void dispatch( Functor functor ) { static Dispatcher< decltype(functor), TypeList > dispatcher( functor ); accept( dispatcher ); };};#define DISPATCHED( TYPE, TYPE_LIST ) \ void accept( AbstractVisitors< TYPE_LIST >& visitor ) override \ { \ static_cast< AbstractVisitor< TYPE >& >( visitor ).visit( *this ); \ }
Затем наследуем AbstractObject от класса Dispatchable. А в классы Object1 и Object2 добавляем макрос DISPATCHED.
Source:
struct Object1;struct Object2;using ObjectList = TypeList< Object1, Object2 >;struct AbstractObject : Dispatchable< ObjectList >{};struct Object1 : AbstractObject{ DISPATCHED( Object1, ObjectList )};struct Object2 : AbstractObject{ DISPATCHED( Object2, ObjectList )};
Отлично, мы спрятали все функции-члены accept и вынесли общий код. Вот теперь все готово.
Пример использования:
Source:
void test( Object1& obj ){ std::cout << "1" << std::endl;}template< class T >void test( T& obj ){ std::cout << "2" << std::endl;}int main(){ Object1 t1,t2,t3,t4; Object2 e1,e2,e3; std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 }; for( auto* obj : vector ) { obj->dispatch( []( auto& obj ) { test(obj); } ); }}
1
2
1
1
2
2
1
Заключение
-
Чтобы добавить или удалить класс, который будет обрабатываться посетителем, достаточно просто изменить список типов.
-
Посетитель не привязан к предметной области, т.к. является шаблоном класса.
-
Можем в полной мере пользоваться шаблонами функций.
Какие недостатки?
-
Дополнительная косвенность, т.к. Dispatcher содержит функтор.
Ссылка на код в compiler explorer.
Full source:
#include <type_traits>#include <iostream>#include <vector>template< class ... T >struct TypeList{};template< class T >struct AbstractVisitor{ virtual ~AbstractVisitor() = default; virtual void visit( T& ) = 0;};template< class ...T >struct AbstractVisitors;template< class ... T >struct AbstractVisitors< TypeList< T... > > : virtual AbstractVisitor< T >...{};template< class Dispatcher, class T >struct Resolver : virtual AbstractVisitor< T >{ void visit( T& obj ) override { static_cast< Dispatcher* >( this )->functor( obj ); };};template< class Functor, class ... T >struct Dispatcher;template< class Functor, class ... T >struct Dispatcher< Functor, TypeList< T... > > : AbstractVisitors< TypeList< T... > >, Resolver< Dispatcher< Functor, TypeList< T... > >, T >...{ Dispatcher( Functor functor ) : functor( functor ) {} Functor functor;};template< class TypeList >struct Dispatchable{ virtual ~Dispatchable() = default; virtual void accept( AbstractVisitors< TypeList >& ) = 0; template< class Functor > void dispatch( Functor functor ) { static Dispatcher< decltype(functor), TypeList > dispatcher( functor ); accept( dispatcher ); };};#define DISPATCHED( TYPE, TYPE_LIST ) \ void accept( AbstractVisitors< TYPE_LIST >& visitor ) override \ { \ static_cast< AbstractVisitor< TYPE >& >( visitor ).visit( *this ); \ }struct Object1;struct Object2;using ObjectList = TypeList< Object1, Object2 >;struct AbstractObject : Dispatchable< ObjectList >{};struct Object1 : AbstractObject{ DISPATCHED( Object1, ObjectList )};struct Object2 : AbstractObject{ DISPATCHED( Object2, ObjectList )};void test( Object1& obj ){ std::cout << "1" << std::endl;}template< class T >void test( T& obj ){ std::cout << "2" << std::endl;}int main(){ Object1 t1,t2,t3,t4; Object2 e1,e2,e3; std::vector< AbstractObject* > vector = { &t1, &e1, &t2, &t3, &e2, &e3, &t4 }; for( auto* obj : vector ) { obj->dispatch( []( auto& obj ) { test(obj); } ); }}