2017-06-02 45 views
5

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żenie

  • gdy 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ć?

+1

A [MCVE] odtwarzając ostrzeżenia kompilatora, które chcesz wyciszyć byłoby miło. –

+0

Operator przecinka jest zawsze gotowy do nadużyć: http://coliru.stacked-crooked.com/a/78f96318349b604b –

Odpowiedz

2

Chyba jeden sposób to zrobić byłoby napisać kilka specjalizacje przypadku bazowego

zamiast robić to, można izolować swoje „oddziały kompilacji” do funkcji, która zajmuje się wyłącznie wywołanie on_error i wywołanie tej nowej funkcji zamiast on_error wewnątrz 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(); 
} 
+1

1. Co jeśli 'f' zwraca' void'? 2. Dlaczego 'on_error' nie może zwrócić czegoś wymienialnego na typ wyniku? –

+0

Zasadniczo to właśnie zrobiłem, z tym wyjątkiem, że zostawiłem warunek SFINAE z gałęzi 'is_same_v', aby działało, gdyby' on_error' zwrócił coś przekonwertowalnego na typ wyniku, i tak, że byłoby to trudne w drugim przypadku. ... Ale T.C. zwraca uwagę na "co jeśli' f' zwróci void? ", więc teraz będę musiał to zmienić. :) – Quuxplusone

1

Jak na temat korzystania variant (std C++ 17, lub zwiększyć jeden)? (I używać użytkownik statycznego)

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

+0

Nie, zastępuje się polimorficzną hierarchię z nie-polimorficznym wariantem. – Quuxplusone

+0

Czy ustalono hierarchię? jeśli tak, nadal możesz zastosować [wzór gościa] (https://stackoverflow.com/documentation/design-patterns/4579/visitor-pattern/15127/visitor-pattern-example-in-c#t=201706022117345811126). (a następnie prawdopodobnie [wiele wysyłek] (https://stackoverflow.com/a/29345504/2684539)). – Jarod42

+0

Wierzę, że na to pytanie odpowiada moje zdanie w moim OP: "Na przykład, jeśli chcę dodać' bool does_be_greater (BaseValue *, UserInput) ', to wymagałoby to całej innej wirtualnej metody z N implementacjami rozproszonymi w hierarchii. " Będę uczyć się https: // stackoverflow.com/questions/29286381/multiple-dispatch-solution-with-full-maintainability/29345504 # 29345504 i sprawdź, czy ma to zastosowanie. – Quuxplusone