Joel Holdsworth
Joel Holdsworth

Reputation: 363

template function pointers and lambdas

Hi I'm trying to sort out an issue with the following code:

template<typename... Args>
using Func = void (*)(Args... args);

template<typename... Args>
void do_test(Func<Args&...> f, Args&... args) {
    for (int i = 0; i != 100; i++)
      f(args...);
}

int main(){
    int x = 0;
    do_test(Func<int&>([](int &y) { y++; }), x);  // OK

    // Error - mismatched types 'void (*)(Args& ...)' and 'main()::<lambda(int&)>'
    do_test([](int &y) { y++; }, x);

    return x;
}

https://godbolt.org/z/UaXxFJ

Can anyone explain why it is necessary to wrap the lambda in Func<int&>( )? Is there a way to avoid it? - because if the argument list is non-trivial it becomes quite tedious having to list out the argument types twice.

The goal here is to make a visitor pattern that the compiler can optimize away. I'm using it for an image processing algorithm where I want to reuse the code of the outer loops, with various bits of inner code. The Args are being used as something similar to a lambda capture, except using traditional function pointers so that the compiler can optimize them away - which it seems not to be able to do with std::function<>

Upvotes: 4

Views: 760

Answers (1)

walnut
walnut

Reputation: 22152

You can just allow the function to take any type, whether function pointer or lambda:

template<typename F, typename... Args>
void do_test(F f, Args&... args) {
    for (int i = 0; i != 100; i++)
      f(args...);
}

Depending on your use case, consider taking f by-const-reference or forwarding reference (i.e. F&&), as well.

You should also consider changing the way you take the args function parameters. Usually in such a situation you would take them by forwarding reference, meaning Args&&... args instead of Args&... args. Otherwise you won't be able to call the function with rvalues arguments.


Or if you have some specific reason to accept only this one specific function pointer type, you can make the first function parameter a non-deduced context, given that the template arguments can already be deduced from the other function parameters:

template<typename... Args>
void do_test(std::type_identity_t<Func<Args&...>> f, Args&... args) {
    for (int i = 0; i != 100; i++)
      f(args...);
}

std::type_identity_t is a C++20 feature, but can be easily implemented:

template<typename T>
struct type_identity {
    using type = T;
};

template<typename T>
using type_identity_t = typename type_identity<T>::type;

Everything left to the scope resolution operator :: in type_identity<T>::type is a non-deduced context and so the first function parameter will not be used to deduce the Args, which in turn means that implicit conversions will be considered (e.g. the lambda to function pointer conversion).

Alternatively, as mentioned by @FrançoisAndrieux in the question comments, you can use the lambda + trick to convert the lambda to a function pointer at the call site:

do_test(+[](int &y) { y++; }, x);

Also note that taking a function pointer of this specific type means that the function can only be called with functions that have exactly this type. For example args is always deduced to a reference type, so any possible function than may be used with this one must take only reference parameters. This is usually not what you want. Usually you want the loose behavior of std::function<R(Args...)>, which can be constructed from any function object that is callable with the specified Args and returns something that can be implicitly converted to R.

Upvotes: 6

Related Questions