c++templateslambdac++17arity

Specializing function template based on lambda arity


I am trying to specialize a templated function based on the arity of the lambda that I pass to it as an argument. This is what I have come up with for a solution:

template<typename Function, bool>
struct helper;

template<typename Function>
struct helper<Function, false>
{
    auto operator()(Function&& func)
    {
        std::cout << "Called 2 argument version.\n";
        return func(1, 2);
    }
};

template<typename Function>
struct helper<Function, true>
{
    auto operator()(Function&& func)
    {
        std::cout << "Called 3 argument version.\n";
        return func(1, 2, 3);
    }
};

template<typename T>
struct B
{
    T a;
    const T someVal() const { return a; }
};

template<typename Function, typename T>
auto higherOrderFun(Function&& func, const T& a)
{
    return helper<Function, std::is_invocable<Function, decltype(a.someVal()), decltype(a.someVal()), decltype(a.someVal())>::value>{}(std::forward<Function>(func));
}


int main()
{
    B<int> b;
    std::cout << higherOrderFun([](auto x, auto y) {return x+y; }, b) << "\n";
    std::cout << higherOrderFun([](auto x, auto y, auto z) {return x + y+z; }, b) << "\n";
    return 0;
}

Is there a way to achieve this in a more elegant manner? I've looked through this: Arity of a generic lambda

However, the latest solution (florestan's) turns all arguments into aribtrary_t, so one has to cast them back inside of each lambda, which I do not find ideal. Ideally I would have liked to directly specialize the templated higherOrderFun with SFINAE, but as it is I use a helper class in order to achieve that. Is there a more straighforward way? For instance to apply SFINAE directly to higherOrderFun without relying on a helper class? The whole point of this is to not have to change higherOrderFun into higherOrderFun2 and higherOrderFun3, but rather have the compiler deduce the correct specialization from the lambda and the given argument (const T& a).

I should mention that I also don't care about the type of the arguments to the function - just about their count, so I would have changed decltype(a.someVal()) to auto in my example if that was possible (maybe there's a way to circumvent explicitly defining the types?).


Solution

  • 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);
        }
        // ...
    }