Specializing function template based on lambda arity
Asked Answered
S

2

6

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

Shorthorn answered 5/7, 2019 at 1:20 Comment(6)
What if func can both accept two arguments and three arguments?Travesty
"aribtrary_t, so one has to cast them back inside of each lambda". No it is only used in un-evaluated context (so don't instantiate the generic lambdas if any).Astoria
@Astoria I tried using his arity_v for SFINAE in msvc2017 and it required the cast inside each lambda since it apparently instantiated it with arbitrary_t when I used arity_v. I could try to create a minimal example when I get back though.Shorthorn
How, indeed its traits isn't working as we might expect Demo.Astoria
@Astoria Yes, this is precisely what I meant. Thank you for making an example, I already mentioned this in the original thread.Shorthorn
@L.F. In the specific case I have in mind, I do not expect such functions, so this is a non-issue in this case. Ideally however, one should be able to query if those exist. This means that a desirable behaviour would for example return the maximum arity, and there would be additional arity traits that would be used to test for the existence of an overload with a specific arity (for example arity_v<F,2>). But as I said - this is not specifically regarding my problem, it's just something I believe would be useful in general. My problem would have been easily solved if there was airty<F,N>.Shorthorn
A
2

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);
    }
    // ...
}
Astoria answered 5/7, 2019 at 8:15 Comment(2)
Is there any way to achieve this if I decide to not directly return the result of the function? Note also that the 1,2,3 were simply placeholders - in general the arguments would be taken possibly from a.someVal() or something similar. (I should have clarified this in hindsight). I like the priority idea.Shorthorn
Yes, you might replace 1, 2, 3 by a.someval().Astoria
I
3

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;
}
Interplead answered 5/7, 2019 at 2:30 Comment(2)
Notice that OP uses generic lambda, so your solution won't work (as for any overloaded operator() BTW).Astoria
Unfortunately a direct copy-paste of your code into msvc, or godbolt's latest msvc ver (19.21) fails to compile. The first part of the error reads: 'n_extract_callable_parameters<T,_Ret (__thiscall std::_Func_class<_Ret,int>::* )(int) const>': base class undefined I am unsure whether you're doing something that goes against the standard, or if it's msvc that doesn't support the standard adequately. But eitherway it fails on a major compiler.Shorthorn
A
2

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);
    }
    // ...
}
Astoria answered 5/7, 2019 at 8:15 Comment(2)
Is there any way to achieve this if I decide to not directly return the result of the function? Note also that the 1,2,3 were simply placeholders - in general the arguments would be taken possibly from a.someVal() or something similar. (I should have clarified this in hindsight). I like the priority idea.Shorthorn
Yes, you might replace 1, 2, 3 by a.someval().Astoria

© 2022 - 2024 — McMap. All rights reserved.