What is the best way to implement this answer "on error, quit"?

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(); } }; // and so on, with overrides for each child class... bool does_equal(BaseValue *bp, UserInput u) { return bp->is_equal(u); } 

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?

+5
source share
2 answers

I guess one way to do this is to write a few base case specializations

Instead, you can isolate your compile-time branches to a function that deals exclusively with the on_error call, and call this new function instead of on_error inside visit_impl::call .

 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 error_dispatch<F, Derived>(on_error); // ^^^^^^^^^^^^^^^^^^^^^^^^^ } } 

 template <typename F, typename Derived, typename E> auto error_dispatch(const E& on_error) -> std::enable_if_t<is_void_v<decltype(on_error())>> { on_error(); throw Dummy(); } template <typename F, typename Derived, typename E> auto error_dispatch(const E& on_error) -> std::enable_if_t< is_same_v<decltype(on_error()), decltype(std::declval<const F&>()(Derived{}))> > { return on_error(); } 
+2
source

What about using variant (std C ++ 17, or boost one)? (and use static visitor)

 using BaseValue = std::variant<int, double, std::string>; struct bin_op { void operator() (int, double) const { std::cout << "int double\n"; } void operator() (const std::string&, const std::string&) const { std::cout << "strings\n"; } template <typename T1, typename T2> void operator() (const T1&, const T2&) const { std::cout << "other\n"; /* Or throw */ } }; int main(){ BaseValue vi{42}; BaseValue vd{42.5}; BaseValue vs{std::string("Hello")}; std::cout << (vi == vd) << std::endl; std::visit(bin_op{}, vi, vd); std::visit(bin_op{}, vs, vs); std::visit(bin_op{}, vi, vs); } 

Demo

+1
source

Source: https://habr.com/ru/post/1268485/


All Articles