漂亮的 sfinae static_assert
Pretty sfinae with static_assert
我正在尝试创建一个将注册接收者的事件管理器。为此,我希望能够使用给定参数构造一个 std::function
。但是,我希望最终用户能够轻松理解该错误。我想用 SFINAE 和依赖类型 static_assert
来做这件事,但我遇到了麻烦,因为这两个函数在有效输入上变得不明确。此外,我希望用户可以收到多个错误原因。由于有两个失败点(提供无效的仿函数和提供错误的事件类型),我希望总共有 3 个函数,第一个是正确输入的函数,然后是不正确的输入(而不是有 4 个函数用于每个状态的组合)。
这可以用 c++17 解决 if constexpr
但我的目标平台是 c++14 所以需要使用其他方法。
我目前的尝试(只检查一种错误状态):
template <typename Event, typename Func>
auto register(Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
void register(Func &&) {
static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}
meta::delay_v
等于 false
但取决于其参数,因此 static_assert
在函数被调用之前不会被触发。
一个更复杂的用例是
template <typename Event, typename Func>
auto register(Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func))
,meta::is_in_tuple<Event, Events_Tuple>
,void()) {}
因此,如果第一个测试失败(func_t
构造),那么我们将 static_assert
解决这个问题,如果第二个测试失败,我们将 static_assert
解决这个问题。因此,如果第一个测试失败,无论第二个测试如何,我们都会使一些静态断言失败。然后,如果第一个测试通过,我们将打印第二个测试失败的信息。不必重写测试将是一个非常好的奖励。
当条件满足时,它们实际上是模棱两可的,因为两者都是有效的。
只有第一个函数有一个可以禁用它的sfinae表达式,因此第二个函数总是一个可行的解决方案(满足条件时是一个模糊的解决方案)。
您可以这样做:
template <typename Event, typename Func>
auto register(int, Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
void register(char, Func &&) {
static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}
template <typename Event, typename Func>
void register(Func &&func) {
register<Event>(0, std::forward<Func>(func));
}
在这种情况下,0
(即 int
)将强制编译器选择第一个函数并进行尝试。如果可行,则没有歧义(第二个想要 char
),否则 0
可以转换为 char
并用于调用第二个函数。
如果你有两个以上的条件,你可以这样做:
template<int N> struct tag: tag<N-1> {};
template<> struct tag<0> {};
template <typename Event, typename Func>
auto register(tag<2>, Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
auto register(tag<1>, Func &&func)
-> decltype(func_alternative_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
void register(tag<0>, Func &&) {
static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}
template <typename Event, typename Func>
void register(Func &&func) {
register<Event>(tag<2>{}, std::forward<Func>(func));
}
解决方案的数量越多,使用的标签数量就越多。适用于 int
/char
技巧的相同原理在这里起作用。
作为旁注,正如@StoryTeller 在评论中提到的,请注意 register
是保留关键字,您不应在生产代码中使用它。
经过一番思考,我发现了另一种结构看起来更好的方法。
template <typename Func, typename... Ts>
decltype(auto) evaluate_detector(std::true_type, Func && f, Ts&&...) {
return f(true);
}
template <typename Func, typename... Ts>
decltype(auto) evaluate_detector(std::false_type, Func &&, Ts&&... ts) {
return evaluate_detector(std::forward<Ts>(ts)...);
}
template <typename Event, typename Func>
void register(Func &&func) {
using can_construct = std::is_constructable<func_t<Event>, Func>;
using proper_event = meta::is_in_tuple<Event, Events_Tuple>;
evaluate_detector(meta::and<can_construct, proper_event>{},
[&](auto){/*do proper thing*/};
meta::not<can_construct>{},
[](auto delay){static_assert(meta::delay_v<decltype(delay)>, "can't construct"},
meta::not<proper_event>{},
[](auto delay){static_assert(meta::delay_v<decltype(delay)>, "improper event"});
}
优点是将所有错误状态集中在一个中央位置,而不必创建许多被覆盖的函数。这就是我设想使用检测器习惯用法的方式。 can_construct
和 proper_event
的类型评估为 std::true_type
和 std::false_type
,或者继承这些类型的东西,所以我们仍然有重载决议,但以通用方式完成。
注意:这开始是对 的评论,但后来变得有点大;为衍生而道歉。
我建议重组如下:
namespace detail {
template<typename PredT, typename F>
struct fail_cond {
using pred_type = PredT;
F callback;
};
struct success_tag { };
template<typename F>
constexpr decltype(auto) eval_if(int, fail_cond<success_tag, F>&& fc) {
return fc.callback();
}
template<
typename FC, typename... FCs,
typename PredT = typename std::decay_t<FC>::pred_type,
std::enable_if_t<std::is_base_of<std::false_type, PredT>{}, int> = 0
>
constexpr decltype(auto) eval_if(int, FC&& fc, FCs&&...) {
return fc.callback(PredT{});
}
template<typename FC, typename... FCs>
constexpr decltype(auto) eval_if(long, FC&&, FCs&&... fcs) {
return detail::eval_if(0, std::move(fcs)...);
}
}
template<typename PredT, typename F, typename = std::result_of_t<F&(std::true_type)>>
constexpr detail::fail_cond<PredT, F> fail_cond(F&& failure_cb) {
return {std::forward<F>(failure_cb)};
}
template<typename F, typename... PredTs, typename... Fs>
constexpr decltype(auto) eval_if(F&& success_cb, detail::fail_cond<PredTs, Fs>&&... fcs) {
return detail::eval_if(
0, std::move(fcs)...,
detail::fail_cond<detail::success_tag, F>{std::forward<F>(success_cb)}
);
}
用法现在如下所示:
template<typename Event, typename Func>
decltype(auto) register(Func&& func) {
using can_construct = std::is_constructible<func_t<Event>, Func&&>;
using proper_event = meta::is_in_tuple<Event, Events_Tuple>;
return eval_if(
[&]() { /*do proper thing*/ },
fail_cond<can_construct>([](auto pred) { static_assert(pred, "can't construct"); }),
fail_cond<proper_event>([](auto pred) { static_assert(pred, "improper event"); })
);
}
// or ...
template<typename Event, typename Func>
decltype(auto) register(Func&& func) {
return eval_if(
[&]() { /*do proper thing*/ },
fail_cond<std::is_constructible<func_t<Event>, Func&&>>(
[](auto pred) { static_assert(pred, "can't construct"); }
),
fail_cond<meta::is_in_tuple<Event, Events_Tuple>>(
[](auto pred) { static_assert(pred, "improper event"); }
)
);
}
该演示虽然设计得很痛苦,但显示了在 compile-time 或 运行时出现故障行为的可能性(n.b。故障回调可以 return值)。还证明了传递给失败回调的值是失败谓词的一个实例,它允许潜在更丰富的失败行为并减少 static_assert
s.
所需的样板文件
我正在尝试创建一个将注册接收者的事件管理器。为此,我希望能够使用给定参数构造一个 std::function
。但是,我希望最终用户能够轻松理解该错误。我想用 SFINAE 和依赖类型 static_assert
来做这件事,但我遇到了麻烦,因为这两个函数在有效输入上变得不明确。此外,我希望用户可以收到多个错误原因。由于有两个失败点(提供无效的仿函数和提供错误的事件类型),我希望总共有 3 个函数,第一个是正确输入的函数,然后是不正确的输入(而不是有 4 个函数用于每个状态的组合)。
这可以用 c++17 解决 if constexpr
但我的目标平台是 c++14 所以需要使用其他方法。
我目前的尝试(只检查一种错误状态):
template <typename Event, typename Func>
auto register(Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
void register(Func &&) {
static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}
meta::delay_v
等于 false
但取决于其参数,因此 static_assert
在函数被调用之前不会被触发。
一个更复杂的用例是
template <typename Event, typename Func>
auto register(Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func))
,meta::is_in_tuple<Event, Events_Tuple>
,void()) {}
因此,如果第一个测试失败(func_t
构造),那么我们将 static_assert
解决这个问题,如果第二个测试失败,我们将 static_assert
解决这个问题。因此,如果第一个测试失败,无论第二个测试如何,我们都会使一些静态断言失败。然后,如果第一个测试通过,我们将打印第二个测试失败的信息。不必重写测试将是一个非常好的奖励。
当条件满足时,它们实际上是模棱两可的,因为两者都是有效的。
只有第一个函数有一个可以禁用它的sfinae表达式,因此第二个函数总是一个可行的解决方案(满足条件时是一个模糊的解决方案)。
您可以这样做:
template <typename Event, typename Func>
auto register(int, Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
void register(char, Func &&) {
static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}
template <typename Event, typename Func>
void register(Func &&func) {
register<Event>(0, std::forward<Func>(func));
}
在这种情况下,0
(即 int
)将强制编译器选择第一个函数并进行尝试。如果可行,则没有歧义(第二个想要 char
),否则 0
可以转换为 char
并用于调用第二个函数。
如果你有两个以上的条件,你可以这样做:
template<int N> struct tag: tag<N-1> {};
template<> struct tag<0> {};
template <typename Event, typename Func>
auto register(tag<2>, Func &&func)
-> decltype(func_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
auto register(tag<1>, Func &&func)
-> decltype(func_alternative_t<Event>(std::forward<Func>(func)), void()) {}
template <typename Event, typename Func>
void register(tag<0>, Func &&) {
static_assert(meta::delay_v<Func>, "Function object cant be constructed by function");
}
template <typename Event, typename Func>
void register(Func &&func) {
register<Event>(tag<2>{}, std::forward<Func>(func));
}
解决方案的数量越多,使用的标签数量就越多。适用于 int
/char
技巧的相同原理在这里起作用。
作为旁注,正如@StoryTeller 在评论中提到的,请注意 register
是保留关键字,您不应在生产代码中使用它。
经过一番思考,我发现了另一种结构看起来更好的方法。
template <typename Func, typename... Ts>
decltype(auto) evaluate_detector(std::true_type, Func && f, Ts&&...) {
return f(true);
}
template <typename Func, typename... Ts>
decltype(auto) evaluate_detector(std::false_type, Func &&, Ts&&... ts) {
return evaluate_detector(std::forward<Ts>(ts)...);
}
template <typename Event, typename Func>
void register(Func &&func) {
using can_construct = std::is_constructable<func_t<Event>, Func>;
using proper_event = meta::is_in_tuple<Event, Events_Tuple>;
evaluate_detector(meta::and<can_construct, proper_event>{},
[&](auto){/*do proper thing*/};
meta::not<can_construct>{},
[](auto delay){static_assert(meta::delay_v<decltype(delay)>, "can't construct"},
meta::not<proper_event>{},
[](auto delay){static_assert(meta::delay_v<decltype(delay)>, "improper event"});
}
优点是将所有错误状态集中在一个中央位置,而不必创建许多被覆盖的函数。这就是我设想使用检测器习惯用法的方式。 can_construct
和 proper_event
的类型评估为 std::true_type
和 std::false_type
,或者继承这些类型的东西,所以我们仍然有重载决议,但以通用方式完成。
注意:这开始是对
我建议重组如下:
namespace detail {
template<typename PredT, typename F>
struct fail_cond {
using pred_type = PredT;
F callback;
};
struct success_tag { };
template<typename F>
constexpr decltype(auto) eval_if(int, fail_cond<success_tag, F>&& fc) {
return fc.callback();
}
template<
typename FC, typename... FCs,
typename PredT = typename std::decay_t<FC>::pred_type,
std::enable_if_t<std::is_base_of<std::false_type, PredT>{}, int> = 0
>
constexpr decltype(auto) eval_if(int, FC&& fc, FCs&&...) {
return fc.callback(PredT{});
}
template<typename FC, typename... FCs>
constexpr decltype(auto) eval_if(long, FC&&, FCs&&... fcs) {
return detail::eval_if(0, std::move(fcs)...);
}
}
template<typename PredT, typename F, typename = std::result_of_t<F&(std::true_type)>>
constexpr detail::fail_cond<PredT, F> fail_cond(F&& failure_cb) {
return {std::forward<F>(failure_cb)};
}
template<typename F, typename... PredTs, typename... Fs>
constexpr decltype(auto) eval_if(F&& success_cb, detail::fail_cond<PredTs, Fs>&&... fcs) {
return detail::eval_if(
0, std::move(fcs)...,
detail::fail_cond<detail::success_tag, F>{std::forward<F>(success_cb)}
);
}
用法现在如下所示:
template<typename Event, typename Func>
decltype(auto) register(Func&& func) {
using can_construct = std::is_constructible<func_t<Event>, Func&&>;
using proper_event = meta::is_in_tuple<Event, Events_Tuple>;
return eval_if(
[&]() { /*do proper thing*/ },
fail_cond<can_construct>([](auto pred) { static_assert(pred, "can't construct"); }),
fail_cond<proper_event>([](auto pred) { static_assert(pred, "improper event"); })
);
}
// or ...
template<typename Event, typename Func>
decltype(auto) register(Func&& func) {
return eval_if(
[&]() { /*do proper thing*/ },
fail_cond<std::is_constructible<func_t<Event>, Func&&>>(
[](auto pred) { static_assert(pred, "can't construct"); }
),
fail_cond<meta::is_in_tuple<Event, Events_Tuple>>(
[](auto pred) { static_assert(pred, "improper event"); }
)
);
}
该演示虽然设计得很痛苦,但显示了在 compile-time 或 运行时出现故障行为的可能性(n.b。故障回调可以 return值)。还证明了传递给失败回调的值是失败谓词的一个实例,它允许潜在更丰富的失败行为并减少 static_assert
s.