Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphic visitor with lambdas

Tags:

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?

like image 909
Silvan Wegmann Avatar asked Aug 31 '17 11:08

Silvan Wegmann


People also ask

Do lambdas go out of scope?

A lambda object must not outlive any of its reference captured objects. Lambda expressions may capture objects with automatic storage duration from the set of enclosing scopes (called the reaching scope) for use in the lambda's function body.

How do C++ lambdas work?

The context of a lambda is the set of objects that are in scope when the lambda is called. The context objects may be captured then used as part of the lambda's processing. Capturing an object by name makes a lambda-local copy of the object. Capturing an object by reference allows the lambda to manipulate its context.

What is Visitor Pattern C++?

Visitor in C++ Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code.


2 Answers

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

like image 110
Vittorio Romeo Avatar answered Oct 01 '22 04:10

Vittorio Romeo


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

like image 38
Jarod42 Avatar answered Oct 01 '22 03:10

Jarod42