6
\$\begingroup\$

I've watched a talk by Fedor Pikus on design patterns held at CppCon 2019. Motivated by this, I wrote my own visitor implementation and some helpers which allow me to create a visitor from a couple of lambdas.

My goal is to reduce the amount of boilerplate code but still use the classical visitor pattern (I know about std::variant and std::visit, but that is not what this question is about). The lambdas given to create a new visitor must provide valid overloads for all visitable classes, i.e. it allowed to create a visitor providing a single lamda [](const auto &t) {...}. This is nice, if only one or two visitable objects require special attention but the rest are handled in a default way. On the other hand, one needs to bear overloading resolution rules in mind when constructing a new visitor and possible default behavior might easily lead to bugs, and therefore I am not sure if my implementation is as smart and helpful as I think.

This is the visitor, nothing really exciting here:

#include <functional>
#include <type_traits>

template <class ... Ts> struct Visitor;

template <class Tp>
struct Visitor<Tp>
{
    virtual void visit(const Tp& t) = 0;
    virtual ~Visitor() = default;
};

template <class Tp, class ... Ts>
struct Visitor<Tp, Ts...>
: private Visitor<Tp>
, private Visitor<Ts>...
{
    // make the base class visit functions visible
    using Visitor<Tp>::visit;
    using Visitor<Ts>::visit...;
};

And the implementation of the lambda overloaded visitor:

namespace detail_visitor
{

// Check if a given type is a Visitor<...>
template <class Tp> struct IsVisitor : std::false_type {};
template <class ... Ts> struct IsVisitor<Visitor<Ts...>> : std::true_type {};


// Overload overload functors
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// Explicit deduction guide, not needed as of c++20.
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;


template <class VisitorBase, class List, class Functor>
struct OverloadedVisitorImpl;


// Specialization to expand the visitable types in the given List.
template <class VisitorBase, template<class...> class List, class Tp
    , class...Ts, class OverloadedFunctor>
struct OverloadedVisitorImpl<VisitorBase, List<Tp, Ts...>, OverloadedFunctor>
    : public OverloadedVisitorImpl<VisitorBase, List<Ts...>, OverloadedFunctor>
{
    static_assert (IsVisitor<VisitorBase>::value);

    OverloadedVisitorImpl (OverloadedFunctor&& f)
    : OverloadedVisitorImpl<VisitorBase, List<Ts...>, OverloadedFunctor> {
        std::forward<OverloadedFunctor>(f)}
    { }

    // make the base class visit functions visible
    using VisitorBase::visit;
    void visit(const Tp& t) final {OverloadedFunctor::operator()(t);}
};


// Specialization to end the recursion
template <class VisitorBase, template<class...> class List, class Tp
    , class OverloadedFunctor>
struct OverloadedVisitorImpl<VisitorBase, List<Tp>, OverloadedFunctor>
    : public VisitorBase
    , protected OverloadedFunctor
{
    static_assert (IsVisitor<VisitorBase>::value);

    OverloadedVisitorImpl (OverloadedFunctor&& f)
    : OverloadedFunctor{std::forward<OverloadedFunctor>(f)} {}

    // make the base class visit functions visible
    using VisitorBase::visit;
    void visit(const Tp& t) final {OverloadedFunctor::operator()(t);}
};

}// namespace detail_visitor

template <class VisitorBase, class OverloadedFunctor>
struct OverloadedVisitor
    : public detail_visitor::OverloadedVisitorImpl<VisitorBase, VisitorBase
      , OverloadedFunctor>
{
    using detail_visitor::OverloadedVisitorImpl<VisitorBase, VisitorBase
            , OverloadedFunctor>::OverloadedVisitorImpl;
};

template <class VisitorBase, class...Fs>
auto make_lambda_visitor (Fs&&...fs)
{
    static_assert (detail_visitor::IsVisitor<VisitorBase>::value);

    return OverloadedVisitor<VisitorBase, decltype(detail_visitor::overloaded{fs...})>{
        detail_visitor::overloaded{std::forward<Fs>(fs)...}};
}

Usage:

struct ClassA;
struct ClassB;
struct ClassC;
using IVisitor = Visitor<ClassA, ClassB, ClassC>;
struct Base
{
    virtual void accept(IVisitor& v) const = 0;
    virtual ~Base() = default;
};
struct ClassA: Base {void accept(IVisitor& v) const final {
     v.visit (*this);}};
struct ClassB: Base {void accept(IVisitor& v) const final {
     v.visit (*this);}};
struct ClassC: Base {void accept(IVisitor& v) const final {
     v.visit (*this);}};
int main()
{
    auto v = make_lambda_visitor<IVisitor>(
               [](const ClassA& t){std::cout << "A\n";}, // 1st
               [](const ClassB& t){std::cout << "B\n";}, // 2nd
               [](const auto& t){std::cout << "default\n";}); // 3rd
    ClassA a;
    Base* b = &a;
    b->accept(v); // calls 1st
    ClassC c;
    b = &c;
    b->accept(v); // calls 3rd, since no explicit overload is provided.
    return 0;
}

Live demo

\$\endgroup\$
4
  • \$\begingroup\$ b->accept(v);, virtual void accept(IVisitor& v) const = 0; should these be visit instead of accept? And struct Base() doesn't make sense? \$\endgroup\$
    – user673679
    May 14 at 8:55
  • 1
    \$\begingroup\$ @user673679 Thank you for pointing this out, I fixed the issues and added a demo. Apparently I was not careful enough, when writing the usage example here. \$\endgroup\$ May 14 at 9:13
  • 1
    \$\begingroup\$ I don't think your overloading issues are any different from that experienced by std::visit. \$\endgroup\$
    – JDługosz
    May 14 at 16:17
  • \$\begingroup\$ @JDługosz But do you think this is in general Ok, or would you recommend rather not to use std::visit (and my inplementation)? \$\endgroup\$ May 14 at 18:46

Your Answer

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

Browse other questions tagged or ask your own question.