How to restrict the parameter type of a callable to be passed to a function?

Viewed 160
#include <functional>
template <typename M>
M g(std::function<M(int)> f) {
    return f(0);
}

int main() {
    g([](int x){return x + 1;});
    return 0;
}

I want to express something like "the (only) argument passed to function g should be a callable object that has int as type of the parameter".

G++ 9.3.0 says

prog.cc: In function 'int main()':
prog.cc:8:31: error: no matching function for call to 'g(main()::<lambda(int)>)'
    8 |     g([](int x){return x + 1;});
      |                               ^
prog.cc:3:3: note: candidate: 'template<class M> M g(std::function<M(int)>)'
    3 | M g(std::function<M(int)> f) {
      |   ^
prog.cc:3:3: note:   template argument deduction/substitution failed:
prog.cc:8:31: note:   'main()::<lambda(int)>' is not derived from 'std::function<M(int)>'
    8 |     g([](int x){return x + 1;});
      |                               ^

What is wrong with the above attempt? And how should I achieve that intention?

You may want to see the code snippet on Wandbox here.

3 Answers

Counterintuitively, you're not actually supposed to use std::function to take a function parameter. Instead, you just need a plain template parameter.

Using a plain template parameter works with std::function. But it also works with function pointers, and will let the function use a lambda (or any other class with an operator()) directly, without the performance hit of std::function.

template <typename F>
auto g(F f) -> decltype(f(std::declval<int>())) { //alternatively, `decltype(f(0))`
    return f(0);
}

If f does not have a member function which can take the expression 0 as input, then compilation will fail, so the constraint is enforced. This is actually the case regardless of whether you include the -> decltype part.


Actually answering the question in the title is a bit trickier. There are two things at play:

The first is that a lambda expression is not a std::function instantiation, even if it can be wrapped inside of one. A given concrete std::function instantiation, such as std::function<int(int)>, has a constructor which will convert from a lambda, and that's how we typically use std::function.

However, implicit conversions do not play well with templates. Either a function argument is used to deduce template parameters, in which case that function argument must match, or the function argument is totally concrete, in which case implicit conversions can be performed. But not both, which is what you had tried to do.

First, write a simple meta-function that gives the type of the argument of a unary function pointer:

template<class Ret, class Arg>
auto arg(Ret(*)(Arg)) -> Arg;

Then, in the body of g, you can static_assert that the passed in callable (when decayed to a function pointer), has a single parameter of type int:

template <typename Function>
auto g(Function f) { 
    static_assert(std::is_same_v<decltype(arg(+f)), int>);
    return f(0);
}

Now calls to g will only compile if this constraint is satisfied:

int a(int x) { return x + 1; }
int b(double x) { return x + 1; }

int main() {
    g([](int x){return x + 1;});      // ok
    g([](char x){return x + 1;});     // error
    g([](int x, int){return x + 1;}); // error
    g(a);                             // ok
    g(b);                             // error
}

Here's a demo.

The below code worked for me

#include <iostream>
#include <functional>

template <typename M>
M g(std::function<M (int)> f) {
  return f(0);
}

int main() {
  std::cout << g<int>([](int x){return x + 1; });
  return 0;
}
Related