Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?).

like image 776
lightxbulb Avatar asked Jul 05 '19 01:07

lightxbulb


2 Answers

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;
}
like image 111
Sam Varshavchik Avatar answered Oct 23 '22 04:10

Sam Varshavchik


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);
    }
    // ...
}
like image 36
Jarod42 Avatar answered Oct 23 '22 05:10

Jarod42