警告:在我们找到真正的问题之前,这里有很多背景信息。
我有一个相当宽的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<A,B,C>
只针对typeid
测试A
,如果是,则调用lambda,并调用my::visit<B,C>
如果不。在调用堆栈的底部,my::visit<C>
针对C
测试typeid,如果是,则调用lambda,如果没有则抛出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();}
否则,静态断言
但我觉得我错过了一些更简单的方法。有谁能看到它?
答案 0 :(得分:2)
我想一种方法就是编写基本案例的几个专业
而不是这样做,你可以隔离你的&#34;编译时分支&#34;到专门处理调用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();
}
答案 1 :(得分:1)
如何使用variant
(std 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);
}