5

I want to implement a polymorphic visitor using lambdas without implementing a class. I already have a foundation but am struggling with the type deduction for the parameters of my lambdas.

Let's say I have some legacy code base that decided to use type tags for a polymorphic type like so:

enum class ClassType
{
    BaseType = 0, TypeA, TypeB
};

class BaseType
{
public:
    virtual ~BaseType() {}
    ClassType getType() const
    { return type; }

protected:
    ClassType type;
};

class TypeA : public BaseType
{
public:
    static const ClassType Type = ClassType::TypeA;
    explicit TypeA(int val) : val(val)
    { type = ClassType::TypeA; }
    virtual ~TypeA() {}

    int val;
};

class TypeB : public BaseType
{
public:
    static const ClassType Type = ClassType::TypeB;
    explicit TypeB(std::string s) : s(s)
    { type = ClassType::TypeB; }
    virtual ~TypeB() {}

    std::string s;
};

What I want to achieve is a visitor similar to the std::variant visitors that would then look like this:

std::vector<BaseType*> elements;
elements.emplace_back(new TypeA(1));
elements.emplace_back(new TypeB("hello"));

for (auto elem : elements)
{
    visit(elem,
        [](TypeA* typeA) {
            std::cout << "Found TypeA element, val=" << typeA->val << std::endl;
        },
        [](TypeB* typeB) {
            std::cout << "Found TypeB element, s=" << typeB->s << std::endl;
        }
    );
}

My so far failing approach for implementing such a visit<>() function was the following code:

template <typename T>
struct identity
{
    typedef T type;
};

template <typename T>
void apply_(BaseType* b, typename identity<std::function<void(T*)>&>::type visitor)
{
    if (b->getType() != T::Type)
        return;

    T* t = dynamic_cast<T*>(b);
    if (t) visitor(t);
}

template <typename... Ts>
void visit(BaseType* b, Ts... visitors) {
    std::initializer_list<int>{ (apply_(b, visitors), 0)... };
}

The compiler complains that it cannot deduce the template parameter T for my apply_ function.

How can I declare the correct template and function signature of apply_ to correctly capture lambdas and maybe even other callables? Or is something like this even possible at all?

  • lamdba are not a `std::function`. – Jarod42 Aug 31 '17 at 12:06
  • 2
    With some function traits, you may retrieve argument from `operator ()` assuming no overload, no `auto`. – Jarod42 Aug 31 '17 at 12:07
  • BTW, if you write a real visitor pattern, you may have a way to create your visitor on the fly like you do for `visit`. – Jarod42 Aug 31 '17 at 12:12
  • Possible duplicate of https://stackoverflow.com/questions/7867555/best-way-to-do-variant-visitation-with-lambdas/ ? – Kaz Dragon Aug 31 '17 at 13:27
  • @KazDragon In the referenced entry they use Boost, which I try to avoid in my solution. Several things are pretty similar to my entry but I still think it's a different question. For example I don't need to run one visitor on multiple different values. Also my 'variant' is more of an old-style type-tag based implementation. – Silvan Wegmann Aug 31 '17 at 14:06
  • @Jarod42 A real visitor pattern would certainly be more desirable. In my case I try to implement a visitor without having to add any functions to the original types (`BaseType`, `TypeA`, ...) as they are "3rd-party". As far as I understand for a "real visitor pattern" I would need to do exactly that. – Silvan Wegmann Aug 31 '17 at 14:10

3 Answers3

3

Here's an (incomplete) solution that works with any function object that has an unary, non-overloaded, non-templated operator(). Firstly, let's create an helper type alias to retrieve the type of the first argument:

template <typename> 
struct deduce_arg_type;

template <typename Return, typename X, typename T> 
struct deduce_arg_type<Return(X::*)(T) const>
{
    using type = T;
};

template <typename F>
using arg_type = typename deduce_arg_type<decltype(&F::operator())>::type;

Then, we can use a fold expression in a variadic template to call any function object for which dynamic_cast succeeds:

template <typename Base, typename... Fs>
void visit(Base* ptr, Fs&&... fs)
{
    const auto attempt = [&](auto&& f)
    {
        using f_type = std::decay_t<decltype(f)>;
        using p_type = arg_type<f_type>;

        if(auto cp = dynamic_cast<p_type>(ptr); cp != nullptr)
        {
            std::forward<decltype(f)>(f)(cp);
        }
    };

    (attempt(std::forward<Fs>(fs)), ...);
}

Usage example:

int main()
{
    std::vector<std::unique_ptr<Base>> v;
    v.emplace_back(std::make_unique<A>());
    v.emplace_back(std::make_unique<B>());
    v.emplace_back(std::make_unique<C>());

    for(const auto& p : v)
    {
        visit(p.get(), [](const A*){ std::cout << "A"; },
                       [](const B*){ std::cout << "B"; },
                       [](const C*){ std::cout << "C"; });
    }
}

ABC

live example on wandbox

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • I'm accepting this answer because it turns out the compiler I currently have to use does not yet support the more elaborate pack expansions and/or fold expressions which rules out @Jarod42's answer. And with some minor adaptations to this solution I can get it to compile on my old compiler. – Silvan Wegmann Sep 04 '17 at 11:36
1

Assuming that you cannot change the virtual classes, you may do the following:

template <typename F>
decltype(auto) visitBaseType(BaseType& base, F&& f)
{
    switch (base.getType())
    {
        case ClassType::BaseType: return f(base);
        case ClassType::TypeA: return f(dynamic_cast<TypeA&>(base));
        case ClassType::TypeB: return f(dynamic_cast<TypeB&>(base));
    }
    throw std::runtime_error("Bad type");
}

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

    overloaded(Ts... ts) : Ts(ts)... {}
};
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

template <typename ... Fs>
decltype(auto) visit(BaseType& base, Fs&&... fs)
{
    return visitBaseType(base, overloaded(fs...));
}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

I don't always say this, but this may be a job for the Boost.Preprocessor. You have a list of class types that corresponds to a list of enums, each instance identifies itself via getType(). So we can use that:

#include <boost/preprocessor/seq/for_each.hpp>

#define CLASS_LIST (TypeA) (TypeB)

// just take one visitor
template <class Visitor>
void visit(Base* ptr, Visitor f) {
    switch (ptr->getType()) {
    #define CASE_ST(r, data, elem) case elem: f(static_cast<elem*>(ptr)); break;
    BOOST_PP_SEQ_FOR_EACH(CASE_ST, ~, CLASS_LIST)
    #undef CASE_ST
    default: f(ptr); // in case you want an "else"
                     // this is optional
    }
}

That will preprocess into:

switch (ptr->getType()) {
case TypeA: f(static_cast<TypeA*>(ptr)); break;
case TypeB: f(static_cast<TypeB*>(ptr)); break;
default: f(ptr);
}
Barry
  • 286,269
  • 29
  • 621
  • 977