Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pretty sfinae with static_assert

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.

like image 540
user975989 Avatar asked Jan 16 '17 06:01

user975989


3 Answers

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.

like image 152
skypjack Avatar answered Nov 17 '22 11:11

skypjack


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.

like image 20
user975989 Avatar answered Nov 17 '22 11:11

user975989


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_asserts.

like image 22
ildjarn Avatar answered Nov 17 '22 11:11

ildjarn