Search code examples
c++lambdac++14sfinaegeneric-lambda

Using SFINAE with generic lambdas


Can generic lambdas take advantage of the "Substitution Failure Is Not An Error" rule ? Example

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

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

Are there any workarounds or plans to include this in the language ? Also since generic lambdas are templated function objects under the hood isn't it a bit odd that this can't be done ?


Solution

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