2

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::functions 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.

Community
  • 1
  • 1
Incomputable
  • 2,188
  • 1
  • 20
  • 40
  • Well it could certainly work, but it may not be clear what the behavior of each visitor will be. I could have two visitors of the same type that visit differently under your implementation. – AndyG Dec 18 '15 at 19:53
  • I was thinking about forwarding of arguments. I watched a talk of Scott Meyers' about forwarding and got really frightened. May this be a danger? – Incomputable Dec 18 '15 at 19:56
  • No, forwarding your arguments preserves the l-value or r-value nature of them. There's only danger if you moved from an object you shouldn't have. – AndyG Dec 18 '15 at 19:58

2 Answers2

3

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.

Live example.

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::functions it inherits from all store a std::reverence_wrapper to its contents.

Also missing is copy-construct.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

Your idea of providing a std::function to your visitor at construction faces the challenge of the double dispatch: the visitor has to implement a visting function for every concrete object type it may visit.

It is possible that you can provide a single std::function that meets this challenge (ex: all the concrete elements are derivates of a same base class). But this is not always possible.

Also a visitor is not necessarily stateless. It can maintain a state for evry structure it visits (example: maintaining a count of element or a grand total). While this is easy to code at the level of a visitor class, it's more difficult in a std::function. This means that your visitor implementation would have some limitations in its possible uses.

I'd therefore rather recommend working with a derived visitor class: this is more readable, works even if the concrete elements are unrelated, and gives you more flexibility for example for stateful visitors.

(In this other answer you can find a naive example of an abstract visitor, with a derived concrete stateful visitor working with unrelated concrete elements)

Community
  • 1
  • 1
Christophe
  • 68,716
  • 7
  • 72
  • 138