Как лучше всего реализовать это при ошибке, вызвать обратный вызов?

Будьте осторожны: здесь много справочной информации, прежде чем мы перейдем к настоящему вопросу.

У меня довольно широкая иерархия классов С++ (представляющая что-то вроде выражений разных типов):

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();}

  • в противном случае статическое утверждение

Но я чувствую, что мне не хватает более простого подхода. Кто-нибудь может это увидеть?


person Quuxplusone    schedule 02.06.2017    source источник
comment
Было бы неплохо минимально воспроизводимый пример, воспроизводящий предупреждения компилятора, которые вы хотите отключить.   -  person Vittorio Romeo    schedule 02.06.2017
comment
Оператор запятой всегда готов к злоупотреблениям: coliru.stacked-crooked.com/a/78f96318349b604b< /а>   -  person Johannes Schaub - litb    schedule 03.06.2017


Ответы (2)


Я предполагаю, что один из способов сделать это - написать несколько специализаций базового случая.

Вместо этого вы можете изолировать свои «ветки времени компиляции» в функции, которая имеет дело исключительно с вызовом on_error, и вызывать эту новую функцию вместо on_error внутри 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();
}
person Vittorio Romeo    schedule 02.06.2017
comment
1. что если f вернет void? 2. Почему on_error не может вернуть что-то конвертируемое в тип результата? - person T.C.; 02.06.2017
comment
По сути, это то, что я в конечном итоге сделал, за исключением того, что я оставил условие SFINAE отключенным от ветки is_same_v, чтобы оно работало, если on_error возвращало что-то, конвертируемое в тип результата, и чтобы в другом случае возникала серьезная ошибка. ...Но Т.С. поднимает хороший вопрос о том, что если f вернет void?, так что теперь мне придется это изменить. :) - person Quuxplusone; 03.06.2017

Как насчет использования variant (стандартный С++ 17 или форсированный)? (и использовать статический посетитель)

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);
}

Демо

person Jarod42    schedule 02.06.2017
comment
Нет, замена полиморфной иерархии неполиморфной variant — это правильно. - person Quuxplusone; 03.06.2017
comment
Фиксирована ли иерархия? если да, вы все равно можете применить шаблон посетителя . (а затем, возможно, многократная отправка). - person Jarod42; 03.06.2017
comment
Я полагаю, что на ваш вопрос отвечает это предложение в моем OP: например, если я хочу добавить bool does_be_greater(BaseValue*, UserInput), для этого потребуется совершенно другой виртуальный метод с N реализациями, разбросанными по иерархии. Я изучу stackoverflow. com/questions/29286381/ и посмотрите, применимо ли это. - person Quuxplusone; 03.06.2017
comment
С шаблоном посетителя у вас будет просто новый struct does_be_greater с одной перегрузкой по типу, если вы хотите добавить новую функцию. но если вы хотите добавить ComplexValue, потребуется изменить всех существующих посетителей. - person Jarod42; 03.06.2017
comment
Это имеет смысл; Кажется, я вижу, как это работает технически. Но стилистически с точки зрения строк кода, разве это не огромное количество копий и вставок? struct does_be_greater { bool go(IntValue& p, UserInput u) { return ...; } bool go(DoubleValue& p, UserInput u) { return ...; } ... }; — Есть ли способ избавиться от всего этого шаблонного кода (кроме макросов, конечно)? - person Quuxplusone; 03.06.2017
comment
Вы по-прежнему можете использовать шаблон, если они имеют общую реализацию. - person Jarod42; 03.06.2017