Будьте осторожны: здесь много справочной информации, прежде чем мы перейдем к настоящему вопросу.
У меня довольно широкая иерархия классов С++ (представляющая что-то вроде выражений разных типов):
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; };
А с другой стороны, у меня есть способ привести пользовательский ввод к ожидаемому типу:
class UserInput { template<class T> get_as() const; };
Итак, один из способов написать сопоставитель — «соответствует ли ввод пользователя значению этого BaseValue?» - было бы так:
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);
}
Однако это не масштабируется ни в направлении «ширина иерархии», ни в направлении «количество операций». Например, если я хочу добавить bool does_be_greater(BaseValue*, UserInput)
, для этого потребуется совершенно другой виртуальный метод с N реализациями, разбросанными по иерархии. Поэтому я решил пойти по этому пути:
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();
}
}
На самом деле, я могу выполнить некоторое метапрограммирование и свернуть его в одну функцию visit
, использующую общую лямбду:
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
реализован как "рекурсивный" шаблон функции: my::visit<A,B,C>
просто сравнивает typeid
с A
, вызывает лямбду, если да, и вызывает my::visit<B,C>
, если нет. В нижней части стека вызовов my::visit<C>
сравнивает typeid с C
, вызывает лямбду, если да, и выдает Oops()
, если нет.
Хорошо, теперь по моему актуальному вопросу!
Проблема с my::visit
заключается в том, что поведение при ошибке "throw Oops()
" жестко запрограммировано. Я бы предпочел, чтобы поведение ошибки определялось пользователем, например:
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();
});
}
Проблема, с которой я сталкиваюсь, заключается в том, что когда я это делаю, я не могу понять, как реализовать базовый класс таким образом, чтобы компилятор заткнулся либо из-за несоответствия возвращаемых типов, либо из-за падения с конца функции! Вот версия без обратного вызова 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);
}
И вот что мне очень хотелось бы иметь:
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);
}
То есть я хочу иметь возможность обрабатывать оба этих случая:
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; });
}
Поэтому я предполагаю, что одним из способов сделать это было бы написать несколько специализаций базового случая:
когда
is_void_v<decltype(on_error())>
, используйте{on_error(); throw Dummy();}
, чтобы отключить предупреждение компиляторакогда
is_same_v<decltype(on_error()), decltype(f(Derived{}))>
, используйте{return on_error();}
в противном случае статическое утверждение
Но я чувствую, что мне не хватает более простого подхода. Кто-нибудь может это увидеть?