Be warned: there is a lot of background information before we get to the real issue.
I have a fairly wide hierarchy of C ++ classes (representing something like expressions of different types):
class BaseValue { virtual ~BaseValue(); }; class IntValue final : public BaseValue { int get() const; }; class DoubleValue final : public BaseValue { double get() const; }; class StringValue final : public BaseValue { std::string get() const; };
And on the other hand, I have a way to force the user to the expected type:
class UserInput { template<class T> get_as() const; };
So, one way to write a match is "Does the user enter a value for this BaseValue?" - it would be like this:
class BaseValue { virtual bool is_equal(UserInput) const; }; class IntValue : public BaseValue { int get() const; bool is_equal(UserInput u) const override { return u.get_as<int>() == get(); } };
However, this does not scale either in the “hierarchy width” direction or in the “number of operations” direction. For example, if I want to add bool does_be_greater(BaseValue*, UserInput) , this will require a whole virtual method of zero with N implementations scattered throughout the hierarchy. So I decided to go this route:
bool does_equal(BaseValue *bp, UserInput u) { if (typeid(*bp) == typeid(IntValue)) { return static_cast<IntValue*>(bp)->get() == u.get_as<int>(); } else if (typeid(*bp) == typeid(DoubleValue)) { return static_cast<DoubleValue*>(bp)->get() == u.get_as<double>(); ... } else { throw Oops(); } }
In fact, I can do metaprogramming and collapse this to a single visit function using a common lambda:
bool does_equal(BaseValue *bp, UserInput u) { my::visit<IntValue, DoubleValue, StringValue>(*bp, [&](const auto& dp){ using T = std::decay_t<decltype(dp.get())>; return dp.get() == u.get_as<T>(); }); }
my::visit is implemented as a "recursive" function template: my::visit<A,B,C> just checks typeid for A , calls lambda if it is, and calls my::visit<B,C> if not . At the bottom of the call stack, my::visit<C> checks the typeid in C , calls lambda if it is, and throws Oops() if not.
Ok now for my current question!
The problem with my::visit is that the behavior of the loss of "throw Oops() " is hard-coded. I would prefer the error behavior to be set by the user, for example:
bool does_be_greater(BaseValue *bp, UserInput u) { my::visit<IntValue, DoubleValue, StringValue>(*bp, [&](const auto& dp){ using T = std::decay_t<decltype(dp.get())>; return dp.get() > u.get_as<T>(); }, [](){ throw Oops(); }); }
The problem that I am facing is that when I do this, I can’t figure out how to implement the base class so that the compiler shuts up with either inappropriate return types or with the end of the function dropping! Here is the version without the on_error :
template<class Base, class F> struct visit_impl { template<class DerivedClass> static auto call(Base&& base, const F& f) { if (typeid(base) == typeid(DerivedClass)) { using Derived = match_cvref_t<Base, DerivedClass>; return f(std::forward<Derived>(static_cast<Derived&&>(base))); } else { throw Oops(); } } template<class DerivedClass, class R, class... Est> static auto call(Base&& base, const F& f) { [...snip...] }; template<class... Ds, class B, class F> auto visit(B&& base, const F& f) { return visit_impl<B, F>::template call<Ds...>( std::forward<B>(base), f); }
And here is what I really would like to:
template<class Base, class F, class E> struct visit_impl { template<class DerivedClass> static auto call(Base&& base, const F& f, const E& on_error) { if (typeid(base) == typeid(DerivedClass)) { using Derived = match_cvref_t<Base, DerivedClass>; return f(std::forward<Derived>(static_cast<Derived&&>(base))); } else { return on_error(); } } template<class DerivedClass, class R, class... Est> static auto call(Base&& base, const F& f, const E& on_error) { [...snip...] }; template<class... Ds, class B, class F, class E> auto visit(B&& base, const F& f, const E& on_error) { return visit_impl<B, F>::template call<Ds...>( std::forward<B>(base), f, on_error); }
That is, I want to be able to handle both of these cases:
template<class... Ds, class B, class F> auto visit_or_throw(B&& base, const F& f) { return visit<Ds...>(std::forward<B>(base), f, []{ throw std::bad_cast(); }); } template<class... Ds, class B> auto is_any_of(B&& base) { return visit<Ds...>(std::forward<B>(base), []{ return true; }, []{ return false; }); }
So, I guess one way to do this is to write a few specializations of the base case:
when is_void_v<decltype(on_error())> , use {on_error(); throw Dummy();} {on_error(); throw Dummy();} to disable compiler warning
when is_same_v<decltype(on_error()), decltype(f(Derived{}))> , use {return on_error();}
otherwise static-assert
But I feel that I am missing a simpler approach. Can anyone see this?