Named, static dispatching with std::variant

Viewed 949

I need to fill in some template magic to make the following code snippet to work.

The problem is that I want to be able to define a visitor class for std::variant with named static methods accepting two arguments. How can I fill in Applicator::apply() to make the dispatching work?

struct EventA {};

struct EventB {};

struct EventC {};

using Event = std::variant<EventA, EventB, EventC>;

struct Visitor {
  enum class LastEvent { None, A, B, C };

  struct State {
    LastEvent last_event = LastEvent::None;
  };

  static State apply(State s, EventA e) { return State{LastEvent::A}; }

  static State apply(State s, EventB e) { return State{LastEvent::B}; }
};

template <typename Visitor> struct Applicator {

  static State apply(State s, Event e) {

    /*** Start of pseudo code ***/
    if (Visitor can apply) {
      return Visitor::apply(s, e);
    }
    /*** End of pseudo code ***/

    // Else, don't update state state
    return s;
  }
};

int main() {
  // Handled by visitor
  State s1 = Applicator<Visitor>::apply(State{}, EventA{});
  assert(s1.last_event == Visitor::LastEvent::A);

  // Handled by visitor
  State s2 = Applicator<Visitor>::apply(State{}, EventB{});
  assert(s2.last_event == Visitor::LastEvent::B);

  // NOT handled by visitor
  State s3 = Applicator<Visitor>::apply(State{}, EventC{});
  assert(s3.last_event == Visitor::LastEvent::None);
}
4 Answers

Another solution:

using State = Visitor::State;

template<class Visitor>
struct VisitorProxy {
    State s;

    template<class E>
    auto operator()(E const& e) -> decltype(Visitor::apply(s, e)) {
        return Visitor::apply(s, e);
    }

    template<class E>
    State operator()(E const&) const {
        return s;
    }
};

template <typename Visitor> struct Applicator {
    static State apply(State s, Event e) {
        VisitorProxy<Visitor> p{s};
        return std::visit(p, e);
    }
};

Using the now quite common overloaded class template trick (And Maxim's trick to order the lambdas based on the constness of their operator()) to create a SFINAE-capable functor modeling the logic you're lookig for:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

// ...

template <typename Visitor> struct Applicator {
  static typename Visitor::State apply(typename Visitor::State s, Event e) {
    return std::visit(overloaded{
      [&s](auto e) mutable -> decltype(Visitor::apply(s, e)) { return Visitor::apply(s, e); },
      [&s](auto) { return s; }
    }, e);
  }
};

Note that this ICEs all versions of Clang I've tested on Wandbox, but I haven't found a workaround. Perfect forwarding is left as an exercise to the reader :)

Well, std::is_invocable_r looks like the tool of choice.
Unfortunately, you would have to get the type of the right overload, which would completely defeat the purpose.

Instead, go one step back and use std::is_detected from library fundamentals TS v2 or equivalent and a template:

template <class... Xs>
using can_Visitor_apply = decltype(Visitor::apply(std::declval<Xs>()...));

if constexpr(std::is_detected_convertible<State, can_Visitor_apply, State&, Event&>())
    return Visitor::apply(s, e);

The advantage is that you have a compile-time-constant to hang arbitrary decisions on. The disadvantage is not (yet) having a function which you can simply just call and forget about it.

If the Visitor can always apply, then the code can be as simple as

return std::visit([&](auto e) { return Visitor::apply(s, e); }, e);

But since Visitor cannot always apply, we need to use SFINAE, which requires a set of overloaded function templates. The function templates can be defined like this:

template<class EventType>
static auto applyHelper(State s, EventType e, int)
    -> decltype(Visitor::apply(s, e)) // only enabled if Visitor::apply(s, e) is a valid expression
{
    return Visitor::apply(s, e);
}

template<class EventType>
static State applyHelper(State s, EventType e, long) // long gives a lower precedence
                                                     // in overload resolution when argument is 0
{
    return s;
}

Then the implementation of Applicator::apply can be

  static State apply(State s, Event e) {
      return std::visit([&](auto e) { return applyHelper(s, e, 0); }, e);
  }
Related