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?
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.
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.
Visitor in C++ Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With