Specializing function template based on lambda arity

The following template gives me the number of parameters to a lambda, a std::function, or a plain function pointer. This seems to cover all the basics. So, you specialize on n_lambda_parameters<T>::n, and plug this into your template. Depending on your specific use cases, you may need to employ the facilities offered by std::remove_reference_t or std::decay_t, to wrap this.

Tested with g++ 9. Requires std::void_t from C++17, plenty of examples of simulating std::void_t pre C++17 can be found elsewhere...

#include <functional>

// Plain function pointer.

template<typename T> struct n_func_parameters;

template<typename T, typename ...Args>
struct n_func_parameters<T(Args...)> {

    static constexpr size_t n=sizeof...(Args);
};

// Helper wrapper to tease out lambda operator()'s type.

// Tease out closure's operator()...

template<typename T, typename> struct n_extract_callable_parameters;

// ... Non-mutable closure
template<typename T, typename ret, typename ...Args>
struct n_extract_callable_parameters<T, ret (T::*)(Args...) const> {

    static constexpr size_t n=sizeof...(Args);
};

// ... Mutable closure
template<typename T, typename ret, typename ...Args>
struct n_extract_callable_parameters<T, ret (T::*)(Args...)> {

    static constexpr size_t n=sizeof...(Args);
};

// Handle closures, SFINAE fallback to plain function pointers.

template<typename T, typename=void> struct n_lambda_parameters
    : n_func_parameters<T> {};

template<typename T>
struct n_lambda_parameters<T, std::void_t<decltype(&T::operator())>>
    : n_extract_callable_parameters<T, decltype(&T::operator())> {};


#include <iostream>

void foo(int, char, double=0)
{
}

int main()
{
    auto closure=
        [](int x, int y)
    // With or without mutable, here.
        {
        };

    std::cout << n_lambda_parameters<decltype(closure)>::n
          << std::endl; // Prints 2.

    std::cout << n_lambda_parameters<decltype(foo)>::n
          << std::endl; // Prints 3.

    std::cout << n_lambda_parameters<std::function<void (int)>>::n
          << std::endl; // Prints 1.
    return 0;
}

I would use different overloads:

template<typename Function>
auto higherOrderFun(Function&& func)
-> decltype(std::forward<Function>(func)(1, 2, 3))
{
    return std::forward<Function>(func)(1, 2, 3);
}

template<typename Function>
auto higherOrderFun(Function&& func)
-> decltype(std::forward<Function>(func)(1, 2))
{
    return std::forward<Function>(func)(1, 2);
}

Possibly with overload priority as

 struct low_priority {};
 struct high_priority : low_priority{};

template<typename Function>
auto higherOrderFunImpl(Function&& func, low_priority)
-> decltype(std::forward<Function>(func)(1, 2))
{
    return std::forward<Function>(func)(1, 2);
}

template<typename Function>
auto higherOrderFunImpl(Function&& func, high_priority)
-> decltype(std::forward<Function>(func)(1, 2))
{
    return std::forward<Function>(func)(1, 2);
}

template<typename Function>
auto higherOrderFun(Function&& func)
-> decltype(higherOrderFun(std::forward<Function>(func), high_priority{}))
{
    return higherOrderFun(std::forward<Function>(func), high_priority{});
}

If you want to use the arity traits from florestan, it might result in:

template<typename F>
decltype(auto) higherOrderFun(F&& func)
{
    if constexpr (arity_v<std::decay_t<F>, MaxArity> == 3)
    {
        return std::forward<F>(func)(1, 2, 3);
    }
    else if constexpr (arity_v<std::decay_t<F>, MaxArity> == 2)
    {
        return std::forward<F>(func)(1, 2);
    }
    // ...
}