3
\$\begingroup\$

I've been looking at getting back into C++11 after a few years and came across the idea of implementing the 'inline' visitor pattern. The idea is that you can specify your visitor class locally in the code using lambdas, function pointers and/or std::function. Missing functions would default to a default implementation (D in the code below). I would be interested in the opinion of any C++11 gurus out there about improvements that I can make, or if there is a much better way to do this. Over time I'd like to formalize this simple idea into a much better library, so any contributions would be welcome.

My questions:

  1. Is there a better way to extract the type of the first argument of the function that using my arg_type template
  2. Is there a better way of passing in the default visit implementation other than as a functor in a template template argument?
  3. To me having the nodes reference an interface of the visitor exclusively for their type (ivisitor<mynodetype>) seems sensible, but usually the visitor pattern doesn't separate the visit() implementations into separate interfaces, so I'm wondering if there is a downside to this idea?
  4. Can I avoid storing my visit() implementations in a std::function datamember?

GitHub

#include <functional>
#inline-visitor.hpp

template<typename T>
struct arg_type :
  public arg_type<decltype(&T::operator())> {};

template<typename T>
struct arg_type<void(*)(T&)> : 
  public arg_type<void(T&)> {};

template<typename T, typename C>
struct arg_type<void(C::*)(T&) const > : 
  public arg_type<void(T&)> {};

template<typename T>
struct arg_type<void(T&)> {
  typedef T type;
};

template<typename T>
class ivisitor {
  public:
    virtual ~ivisitor() = default;
    virtual void visit(T& node) = 0;
};

template<typename T, template<typename> class D> 
class visitor : public ivisitor<T>{
  public:
    visitor():
      f_(D<T>()) {
    }
    void visit(T& node) override {
      if(f_) {
        f_(node);
      }        
    }
    void set(std::function<void(T&)> f) {
      f_ = f;
    }
  private:
    std::function<void(T&)> f_;
};

template<template<typename> class D, typename ...T>
class compound_visitor : public visitor<T, D>... {
  public:

    compound_visitor() = default;

    template<typename ...F>
    compound_visitor(F... f) {
      set(f...);
    }
    template<typename F, typename ...Fs>
    void set(F f, Fs... fs) {
      set(f);
      set(fs...); 
    }

    template<typename U>
    void visit(U& node) {
      this->visitor<U, D>::visit(node);
    }

    template<typename F>
    void set(F f) {
      this->visitor<typename arg_type<F>::type, D>::set(f);
    }
};

template<typename C, typename ...F>
auto make_compound_visitor(F... f) -> C {
  return c(f...);
}

Example code follows:

main.cpp

#include "inline_visitor.hpp"
#include <iostream>

template<typename T>
struct output_default {
  void operator()(T&) {
    std::cout << "default";
  }
};

class node1 {
public:
  void accept(ivisitor<node1>& v)   {
    v.visit(*this);
  }
};
class node2 {
public:
  void accept(ivisitor<node2>& v)   {
    v.visit(*this);
  }
};

class node3 {
public:
  void accept(ivisitor<node3>& v)   {
    v.visit(*this);
  }
};

class node4 {
public:
  void accept(ivisitor<node4>& v)   {
    v.visit(*this);
  }
};

int main(int argc, char** argv) {
typedef compound_visitor<output_default, node1, node2, node3, node4> concrete_visitor;
  auto v = make_compound_visitor<concrete_visitor>(
    [](node1& node) -> void { std::cout << "n1";},
    std::function<void(node2&)>([](node2& node) -> void { std::cout << "n2";}),
    +[](node3& node) -> void { std::cout << "n3";}
  );

  node1 n1;
  node2 n2;
  node3 n3;
  node4 n4;

  n1.accept(v);
  n2.accept(v);
  n3.accept(v);
  n4.accept(v);

  return 0;
}
\$\endgroup\$
  • \$\begingroup\$ I guess one response to Q3 is that normally the nodes would be stored in some homogenous list and processed iteratively, requiring them to share a common interface. \$\endgroup\$ – the4thamigo_uk Jan 17 '18 at 6:18
1
\$\begingroup\$

Inheritance

I believe that inheritance based OOP is painful to use in C++ compared to languages like java. In my opinion, static polymorphism is better. Runtime polymorphism is hard to do because C++ has manual memory (and ownership semantics in general) and local variables are strongly typed.

Use cases

Personally I can't see a use case for visitor. They might be useful in conjunction with std::variant, but otherwise I fail to see it being useful.

Too much runtime

If a function is virtual, there is no need to store it in std::function. Too many indirections, and the last one has type erasure, which is heavier mechanism than runtime polymorphism.

Static way

Visitors are usually used to get double dispatch. Templates provide N way dispatch, e.g. the type will keep propagating as long as the call chain is all templated. Lets look at this interesting example from std::visit documentation page.

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

This requires C++17, but not hard to emulate using C++14, using make_overloaded.

Now, the usage:

    std::visit(overloaded {
        [](auto arg) { std::cout << arg << ' '; },
        [](double arg) { std::cout << std::fixed << arg << ' '; },
        [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
    }, v); // v is std::variant

As you can see, types are actually inferred, so the need for std::function just goes away. Usually, templates are good way to go unless it is some highly dynamic environment like GUI, where I would pick Qt framework or, for a language, python, or any other duck typed language.

\$\endgroup\$
  • \$\begingroup\$ Really interesting points. I shall digest these and dig more into std::variant as I am not so familiar with it \$\endgroup\$ – the4thamigo_uk Jan 17 '18 at 6:24
0
\$\begingroup\$

looks interesting! I'd be interested in the details. Have you seen this?

https://accu.org/index.php/articles/2021

Missing functions would default to a default implementation

If you have a closed set of types, I wouldn't recommend missing/default implementations for visited types. One benefit of visiting is that if you add a new type you are forced to write missing implementations for all the visitors without overlooking any.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.