Ostrzegam: tutaj jest dużo informacji podstawowych, zanim dojdziemy do prawdziwego pytania.Jaki jest najlepszy sposób, aby zaimplementować to wywołanie zwrotne "w przypadku błędu, throw"?
Mam dość szeroki klasa C++ hierarchię (reprezentujący coś podobnego wyrażenia różnych typów):
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; };
A z drugiej strony, mam wolną drogę do zmuszenia wejście użytkownika do oczekiwanego typu:
class UserInput { template<class T> get_as() const; };
Tak więc jednym ze sposobów napisania matchera - "czy dane wejściowe użytkownika są równe tej wartości BaseValue?" - byłoby tak:
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);
}
Jednak to nie skaluje, albo w „szerokości” hierarchii kierunku, lub w „liczby operacji” kierunku. Na przykład, jeśli chcę dodać bool does_be_greater(BaseValue*, UserInput)
, wymagałoby to całej, wirtualnej metody z N implementacjami rozproszonymi w hierarchii. Więc zdecydowałem się pójść tą drogą Zamiast:
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();
}
}
W rzeczywistości, można zrobić kilka metaprogramowanie i zwijać, że w dół do jednej funkcji visit
biorąc rodzajowe 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
jest zaimplementowany jako "rekursywny" szablon funkcji: my::visit<A,B,C>
po prostu testuje typeid
przed A
, wywołuje lambdę, jeśli tak, i wywołuje my::visit<B,C>
, jeśli nie. Na dole stosu wywołań, my::visit<C>
testuje typid na C
, wywołuje lambdę, jeśli tak, i zgłasza Oops()
, jeśli nie.
Okay, teraz za moje aktualne pytanie!
Problem z my::visit
polega na tym, że zachowanie błędu "rzut Oops()
" jest zakodowane na stałe. Chciałbym naprawdę wolą mieć zachowanie error być określony przez użytkownika, na przykład:
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();
});
}
Problem mam jest, kiedy to zrobić, nie mogę dowiedzieć się, jak zaimplementować klasę bazową w taki sposób, że kompilator zamknie się z powodu niedopasowanych typów powrotu lub wypadnięcia z końca funkcji!Oto wersja bez on_error
callback:
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);
}
A oto co naprawdę chciałbym mieć:
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);
}
Oznacza to, że chcę, aby być w stanie obsługiwać obu tych przypadkach:
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; });
}
Więc myślę, że jeden sposób to zrobić byłoby napisać kilka specjalności sprawy podstawowej:
gdy
is_void_v<decltype(on_error())>
użyć{on_error(); throw Dummy();}
uciszyć kompilator ostrzeżeniegdy
is_same_v<decltype(on_error()), decltype(f(Derived{}))>
użyć{return on_error();}
inaczej, static-dochodzić
Ale czuję się jakbym brakuje niektórych prostsze podejście. Czy ktoś może to zobaczyć?
A [MCVE] odtwarzając ostrzeżenia kompilatora, które chcesz wyciszyć byłoby miło. –
Operator przecinka jest zawsze gotowy do nadużyć: http://coliru.stacked-crooked.com/a/78f96318349b604b –