I'm somewhat new to ood. Reading GoF's design patterns I found Visitor.
My version of visitor pattern is more concrete than as mentioned in Generic visitor using variadic templates. So, my idea is to create concrete Visitor by having private std::function
s which will be supplied during construction. Then, every visit function will call corresponding private std::function
.
My question: is it good practice to implement visitor as mentioned above or if not, why?
Only cons that come to mind is ambiguity, that is, it will be hard to know what particular instance of the visitor will do on composite.
The way you implement visitor with std::function
visitors is to change the accept part of the element. You lose double dispatch as a cost, but you do abstract the boilerplate of iteration a bit.
Instead of one accept
method on the element, have one accept
per kind of visitation.
When you want to visit things in more than one way in the standard visitor, you write more visitor types, and add new accept
overloads to accept them.
In the std::function
based one, you simply write a new accept
type function with a different name; the name goes in the name of the method, not in the name of the visitor type (because the visitor type is anonymous).
In C++14 with SFINAE std::function
smarts, you can go with one overloaded accept
, but then you'd have to pass in a 'visit tag' to the visitor to determine what kind of visiting it is expecting. This is probably not worth the bother.
A second problem is that std::function
does not support multiple overloads of the argument types. One of the uses of visitor is that we dispatch differently based on the dynamic type of the element -- full double dispatch.
As a concrete case study, imagine 3 kinds of visiting: save, load and display. The main difference between save and display is that display culls things that are not visible (either occluded, or set to not visible).
Under traditional element/visitor, you'd have one accept function with 3 overloads that each take a Saver*
or a Loader*
or a Displayer*
. Each of Saver
Loader
and Displayer
has a bunch of visit(element*)
and visit(derived_element_type*)
methods.
Under std::function
visiting, your element instead has a save(std::function<void(element*)>
and a load(
and a display(
method. No double dispatch is done, because std::function
only exposes one interface.
Now, we can write a std::function
-esque multiple-dispatching overload mechanism if we need it. This is advanced C++ however.
template<class Is, size_t I>
struct add;
template<class Is, size_t I>
using add_t=typename add<Is,I>::type;
template<size_t...Is, size_t I>
struct add<std::index_sequence<Is...>, I>{
using type=std::index_sequence<(I+Is)...>;
};
template<template<class...>class Z, class Is, class...Ts>
struct partial_apply;
template<template<class...>class Z, class Is, class...Ts>
using partial_apply_t=typename partial_apply<Z,Is,Ts...>::type;
template<template<class...>class Z, size_t...Is, class...Ts>
struct partial_apply<Z,std::index_sequence<Is...>, Ts...> {
using tup = std::tuple<Ts...>;
template<size_t I> using e = std::tuple_element_t<I, tup>;
using type=Z< e<Is>... >;
};
template<template<class...>class Z, class...Ts>
struct split {
using left = partial_apply_t<Z, std::make_index_sequence<sizeof...(Ts)/2>, Ts...>;
using right = partial_apply_t<Z, add_t<
std::make_index_sequence<(1+sizeof...(Ts))/2>,
sizeof...(Ts)/2
>, Ts...>;
};
template<template<class...>class Z, class...Ts>
using right=typename split<Z,Ts...>::right;
template<template<class...>class Z, class...Ts>
using left=typename split<Z,Ts...>::left;
template<class...Sigs>
struct functions_impl;
template<class...Sigs>
using functions = typename functions_impl<Sigs...>::type;
template<class...Sigs>
struct functions_impl:
left<functions, Sigs...>,
right<functions, Sigs...>
{
using type=functions_impl;
using A = left<functions, Sigs...>;
using B = right<functions, Sigs...>;
using A::operator();
using B::operator();
template<class F>
functions_impl(F&& f):
A(f),
B(std::forward<F>(f))
{}
};
template<class Sig>
struct functions_impl<Sig> {
using type=std::function<Sig>;
};
which gives you a std::function
that supports multiple signatures (but only one function). To use it, try something like:
functions< void(int), void(double) > f = [](auto&& x){std::cout << x << '\n'; };
which when called with an int
, prints an int, and when called with a double
, prints a double.
As noted, this is advanced C++: I simply included it to note that the language is powerful enough to handle the issue.
With that technique, you can double-dispatch using a std::function
type interface. Your visitor simple has to pass in a callable that can handle every overload you dispatch, and your element has to detail all of the types it expects the visitor to be able to support in its functions
signature.
You will notice that if you implement this, you'll get some really magic polymorphism at the visiting sight. You'll be called with the static type of the thing you are visiting, dynamically, and you'll only have to write one method body. Adding new requirements to the contract happens in one spot (at the interface declaration of the accept method), instead of 2+K like classic visitation (in the accept method, in the interface of the visit type, and in each of the various overloads of the visit class (which can be eliminated with CRTP I'll admit)).
The above functions<Sigs...>
stores N copies of the function. A more optimal one would store tur function once, and N invocation views. That is a touch harder, but only a touch.
template<class...Sigs>
struct efficient_storage_functions:
functions<Sigs...>
{
std::unique_ptr<void, void(*)(void*)> storage;
template<class F> // insert SFINAE here
efficient_storage_functions(F&& f):
storage{
new std::decay_T<F>(std::forward<F>(f)),
[](void* ptr){
delete static_cast<std::decay_t<F>*>(ptr);
}
},
functions<Sigs...>(
std::reference_wrapper<std::decay_t<F>>(
get<std::decay_t<F>>()
)
)
{}
template<class F>
F& get() {
return *static_cast<F*>(storage.get());
}
template<class F>
F const& get() const {
return *static_cast<F const*>(storage.get());
}
};
which next needs to be improved with the small object optimization (to not store the type on the stack) and SFINAE support so it doesn't try to construct from things that are not compatible.
It stores the one copy of the incoming callable in a unique_ptr
, and the myriad of std::function
s it inherits from all store a std::reverence_wrapper
to its contents.
Also missing is copy-construct.