5

Passing a function template as argument to another function template is always a bit tricky. Typically one has to resort to creating a lambda object that goes and calls the original function.

Example

template <typename It>
void f(It, It) {}

void g(std::vector<int>::iterator, std::vector<int>::iterator) {}

template <typename C, typename F>
void callA(C&& c, F callable) {
  return callable(std::begin(c), std::end(c));
}

Problem

If I have a std::vector<int> c, I cannot just pass f along to callA because f is a template and not a function:

callA(c, f); // f has an unresolved overloaded function type and cannot be deduced
callA(c, std::distance); // same problem
callA(c, g); // works because g is a proper function, not a template
callA(c, [](auto a, auto b) {return f(a,b);}); // works again

Even if we help deduce the type of the callable:

template <typename C, template <typename> typename F,
          typename T = std::decay_t<decltype(std::begin(std::declval<C>()))>>
auto callB(C&& c, F<T> callable) {
  return callable(std::begin(c), std::end(c));
}

this fails to compile.

Question

Is there a way to force C++ to deduce the function's type directly without resorting to a lambda function or type-erasure like std::function? Effectively, (how) can I turn a function template into a first-class citizen? That is, have its type deduced.

I'm willing to go to some lengths when defining the higher-order function (see callB versus callA), but not when calling it.

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • Re: (how) can I turn a function template into a first-class citizen?, by lambdas, as you've correctly guessed. Function overloads and template specializations might differ from point to point (due to different includes), hence the need to explicitly say, I'm capturing it here. – lorro Oct 12 '22 at 08:45
  • @lorro Yeah, but the point is that forcing me to wrap a function call in a lambda means the function is *not* a "first-class citizen". It is second class. – bitmask Oct 12 '22 at 08:47
  • Can you use function pointer as parameter? Which could be converted from function templates. – songyuanyao Oct 12 '22 at 08:51
  • @songyuanyao Sure, but how do you get the correct function pointer? – bitmask Oct 12 '22 at 08:56
  • @bitmask What I'm saying, it's not a citizen at all. Consider that, in a.cpp you see the template specialization and in b.cpp you don't see it. What should the capture be? It's different from location to location. – lorro Oct 12 '22 at 08:56
  • @lorro I see what you mean. But the call is explicitly in a particular line in a particular file. So **that's** where you "capture" it. – bitmask Oct 12 '22 at 08:58
  • Your second attempt is incorrect simply because there is no class template (or alias template) that matches `F`. A function has a type, but a function *template* doesn't have a type template. – n. m. could be an AI Oct 12 '22 at 09:43
  • Use std::bind! I'm just joking, but frankly, since C++20 your problem probably is no problem anymore (--and you seem to know that). Just replace `template void f(It, It) {}` beforehand against `auto f = [](It, It) {}`. And if you have no access to `f`, do the lambda-wrapping once inside `callA`. – davidhigh Oct 12 '22 at 12:51

2 Answers2

3

You can change the parameter type to function pointer, which could be converted from function template (with template argument deduced). E.g.

template <typename C>
void callA(C&& c, void(*callable) (std::decay_t<decltype(std::begin(std::declval<C>()))>, std::decay_t<decltype(std::begin(std::declval<C>()))>)) {
  return callable(std::begin(c), std::end(c));
}

Then

std::vector<int> c;
callA(c, f); // template argument It will be deduced as std::vector<int>::iterator for f

LIVE

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • It will not work with non-function callables such as capturing lambdas or `std::function`, but this can be fixed by simply adding the original OP's `callA` as an overload. – n. m. could be an AI Oct 12 '22 at 10:00
  • This seems to work pretty well, only that one has to fix the callable's return type. So, a function like `f` which returns void would require a different `callA` than, say, `std::distance`. – bitmask Oct 12 '22 at 12:36
  • @bitmask This is not necessary, you can add another template parameter for the return type – n. m. could be an AI Oct 12 '22 at 12:50
  • @n.1.8e9-where's-my-sharem. I can't get the inference to work. I can get it to work such that you can say `callA(a, std::distance)` and `callA(a, f)` (by defaulting to `void`). But I don't see a way to get `callA(a, std::distance)` to work automatically. – bitmask Oct 12 '22 at 13:01
  • @bitmask hmm that's right, inference doesn't work. – n. m. could be an AI Oct 12 '22 at 13:31
1

Are you aware of BOOST_HOF_LIFT?

It allows you to lift overloaded and/or templated functions into objects with as easy syntax as auto my_max = BOOST_HOF_LIFT(std::max);.

You could change callA(c, std::distance) to callA(c, BOOST_HOF_LIFT(std::distance)).

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • lift is a macro that defines a lambda around the template – Caleth Oct 12 '22 at 09:12
  • @Caleth, [I know](https://stackoverflow.com/questions/65811716/most-terse-and-reusable-way-of-wrapping-template-or-overloaded-functions-in-func), but not having to write yourself the lambda is a great thing, imho. I mean a range-`for` loop is desugared into a "normal" `for` loop, but isn't it a useful abstraction? – Enlico Oct 12 '22 at 11:26