【问题标题】:What's the best way to implement this "on error, throw" callback?实现这个“错误时抛出”回调的最佳方法是什么?
【发布时间】:2017-11-04 05:12:52
【问题描述】:

请注意:在我们解决真正的问题之前,这里有很多背景信息。

我有一个相当广泛的 C++ 类层次结构(代表不同类型的表达式):

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 采用通用 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 被实现为“递归”函数模板:my::visit&lt;A,B,C&gt; 仅针对 typeid 测试 A,如果是则调用 lambda,如果不是则调用 my::visit&lt;B,C&gt;。在调用堆栈的底部,my::visit&lt;C&gt; 针对 C 测试 typeid,如果是则调用 lambda,否则抛出 Oops()

好的,现在开始我的实际问题!

my::visit 的问题在于错误行为“抛出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&lt;decltype(on_error())&gt; 时,使用{on_error(); throw Dummy();} 使编译器警告静音

  • is_same_v&lt;decltype(on_error()), decltype(f(Derived{}))&gt;时,使用{return on_error();}

  • 否则,静态断言

但我觉得我错过了一些更简单的方法。有人能看吗?

【问题讨论】:

标签: c++ exception c++14 template-meta-programming visitor-pattern


【解决方案1】:

我想一种方法是编写几个基本案例的特化

您可以将“编译时分支”隔离到一个专门处理调用on_error 的函数中,并在visit_impl::call 中调用该新函数而不是on_error,而不是这样做。

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.如果f 返回void 会怎样? 2.为什么on_error不能返回可转换为结果类型的东西?
  • 这基本上是我最终要做的,除了我离开了is_same_v 分支的 SFINAE 条件,这样如果on_error 返回了可转换为结果类型的东西,它就可以工作,所以在另一种情况下会出现硬错误。 ...但是 T.C.提出了一个关于“如果f 返回无效怎么办?”的好观点,所以我现在必须去改变它。 :)
【解决方案2】:

使用variant(标准C++ 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);
}

Demo

【讨论】:

  • 不,用非多态 variant 替换多态层次结构是正确的。
  • 层次结构是否固定?如果是,您仍然可以申请visitor pattern。 (然后可能是multiple dispatch)。
  • 我相信我的 OP 中的这句话回答了您的问题:“例如,如果我想添加 bool does_be_greater(BaseValue*, UserInput),这将需要一个完整的另一个虚拟方法,其中 N 个实现分散在层次结构中。 "不过我会研究stackoverflow.com/questions/29286381/…,看看它是否适用。
  • 使用访问者模式,如果您想添加一个新功能,您将只需要一个新的struct does_be_greater,并按类型重载一个。但是如果要添加ComplexValue,则需要更改所有现有访问者。
  • 有道理;我想我知道它在技术上是如何工作的。但就代码行的风格而言,这不是大量的剪切和粘贴吗? struct does_be_greater { bool go(IntValue&amp; p, UserInput u) { return ...; } bool go(DoubleValue&amp; p, UserInput u) { return ...; } ... }; — 有没有办法消除所有的样板文件(当然,宏除外)?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多