Using SFINAE with generic lambdas

谁说胖子不能爱 提交于 2019-12-04 07:56:08

Lambdas are function objects under the hood. Generic lambdas are function objects with template operator()s.

template<class...Fs>
struct funcs_t{};

template<class F0, class...Fs>
struct funcs_t<F0, Fs...>: F0, funcs_t<Fs...> {
  funcs_t(F0 f0, Fs... fs):
    F0(std::move(f0)),
    funcs_t<Fs...>(std::move(fs)...)
  {}
  using F0::operator();
  using funcs_t<Fs...>::operator();
};
template<class F>
struct funcs_t<F>:F {
  funcs_t(F f):F(std::move(f)){};
  using F::operator();
};
template<class...Fs>
funcs_t< std::decay_t<Fs>... > funcs(Fs&&...fs) {
  return {std::forward<Fs>(fs)...};
}

auto f_all = funcs( f1, f2 ) generates an object that is an overload of both f1 and f2.

auto g_integral = 
  [](auto&& func, auto&& param1, auto&&... params) 
    -> std::enable_if_t< std::is_integral<
        std::decay_t<decltype(param1)>
    >{}>
  {
    // ...
  };

auto g_not_integral =  
 [](auto&& func, auto&& param1, auto&&... params) 
    -> std::enable_if_t< !std::is_integral<
        std::decay_t<decltype(param1)>
    >{}>
{
    // ...
};

auto gL = funcs( g_not_integral, g_integral );

and calling gL will do SFINAE friendly overload resolution on the two lambdas.

The above does some spurious moves, which could be avoided, in the linear inheritance of funcs_t. In an industrial quality library, I might make the inheritance binary rather than linear (to limit instantiation depth of templates, and the depth of the inheritance tree).


As an aside, there are 4 reasons I know of to SFINAE enable lambdas.

First, with new std::function, you can overload a function on multiple different callback signatures.

Second, the above trick.

Third, currying a function object where it evaluates when it has the right number and type of args.

Forth, automatic tuple unpacking and similar. If I'm using continuation passing style, I can ask the passed in continuation if it will accept the tuple unpacked, or the future unbundled, etc.

A generic lambda can only have one body, so SFINAE wouldn't be of much use here.

One solution would be to package the call into a class which can store the result and is specialized on a void return type, encapsulating the void special handling away from your lambda. With a very little overhead, you can do this using the thread library facilities:

auto gL = 
    [](auto&& func, auto&&... params)
    {
        // start a timer
        using Ret = decltype(std::forward<decltype(func)>(func)(
            std::forward<decltype(params)>(params)...));
        std::packaged_task<Ret()> task{[&]{
            return std::forward<decltype(func)>(func)(
                std::forward<decltype(params)>(params)...); }};
        auto fut = task.get_future();
        task();
        // stop timer and print elapsed time
        return fut.get(); 
    };

If you want to avoid the overhead of packaged_task and future, it's easy to write your own version:

template<class T>
struct Result
{
    template<class F, class... A> Result(F&& f, A&&... args)
        : t{std::forward<F>(f)(std::forward<A>(args)...)} {}
    T t;
    T&& get() { return std::move(t); }
};
template<>
struct Result<void>
{
    template<class F, class... A> Result(F&& f, A&&... args)
        { std::forward<F>(f)(std::forward<A>(args)...); }
    void get() {}
};

auto gL = 
    [](auto&& func, auto&&... params)
    {
        // start a timer
        using Ret = decltype(std::forward<decltype(func)>(func)(
            std::forward<decltype(params)>(params)...));
        Result<Ret> k{std::forward<decltype(func)>(func),
            std::forward<decltype(params)>(params)...};
        // stop timer and print elapsed time
        return k.get(); 
    };
Barry

The use of SFINAE is to remove an overload or a specialization from the candidate set when resolving a given function or template. In your case, we have a lambda - that is a functor with a single operator(). There is no overload, so there is no reason to use SFINAE1. The fact that the lambda is generic, which makes its operator() a function template, doesn't change that fact.

However, you don't actually need to differentiate between different return types. If func returns void for the given arguments, you can still return it. You just can't assign it to a temporary. But you don't have to do that either:

auto time_func = [](auto&& func, auto&&... params) {
    RaiiTimer t;
    return std::forward<decltype(func)>(func)(
        std::forward<decltype(params)>(params)...); 
};

Just write an RaiiTimer whose constructor starts a timer and whose destructor stops it and prints the result. This will work regardless of func's return type.

If you need something more complicated than that, then this is one of those cases where you should prefer a functor over a lambda.


1Actually, as Yakk points out, SFINAE could still be quite handy to check if your function is callable period, which isn't the problem you're trying to solve - so in this case, still not very helpful.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!