I am trying to create an event manager that will register receivers. To do this, I want to be able to construct a std::function
with a given parameter. However, I want the error to be easily understandable to the end user. I thought of doing this with SFINAE and a type dependent static_assert
but I am having trouble because the two functions become ambiguous on a valid input. Furthermore, I want to have multiple error reasons that the user can receive. Since there are two failure points (providing an invalid functor and providing the wrong event type) I want there to be a total of 3 functions, the first being the one for proper input and then improper inputs trickling down (instead of having 4 functions for each combination of state).
This would be solvable with c++17's if constexpr
but my target platform is c++14 so other methods will need to be used.
My current attempt (which only checks for one error state):
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
equals to false
but is dependent on its parameters, so the static_assert
is not triggered until the function is called.
A more involved use case would be
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()) {}
And so if the first test fails (the func_t
construction) then we would static_assert
about that, and if the second test fails we would static_assert
about that. So if the first test fails, regardless of the second test, we fail some static assert. Then, if the first test passes, we would print about failing the second test. Not having to rewrite the tests would be a very nice bonus.
They are actually ambiguous when the condition is met, for both are valid.
Only the first function has a sfinae expression that can disable it, thus the second function is always a viable solution (that is an ambiguous one when the condition is met).
You can do this instead:
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));
}
In this case, 0
(that is an int
) will force the compiler to pick the first function up and try it. If it works, there is no ambiguity (the second one wants a char
), otherwise 0
can be converted to a char
and used to invoke the second function.
If you have more than two conditions, you can do this:
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));
}
The greater the number of solutions, the greater the number used with tag. The same principles applied to the int
/char
trick work here.
As a side note, as mentioned by @StoryTeller in the comments, note that register
is a reserved keyword and you should not use it in production code.
After some thought, I have found another method that seems nicer to structure.
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"});
}
The advantage is having all the error states in one central location and not having to create many overwritten functions. This is how I envisioned the detector idiom to be used. The types of can_construct
and proper_event
evaluate to std::true_type
and std::false_type
, or something inheriting those types so we still have overload resolution but done in a generic way.
Note: This started as a comment on the OP's answer but grew a bit large; apologies for being derivative.
I suggest the following restructuring:
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)}
);
}
Usage now looks like the following:
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"); }
)
);
}
Online Demo
The demo, while painfully contrived, shows possibilities for failure behavior at compile-time or runtime (n.b. failure callbacks can return values). Also demonstrated is that the value passed to a failure callback is an instance of the predicate that failed, which allows for potentially richer failure behavior and reduces the boilerplate necessary for static_assert
s.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With