Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Passing std::function object to variadic template

I want to pass a callable (std::function object) into a class Foo. The callable refers to a member method of another class which has arbitrary arguments, hence the Foo must be a variadic template. Consider this code:

struct Bar {
  void MemberFunction(int x) {}
};

template<typename ...Args>
class Foo {
 public:
  Foo(std::function<void(Bar*, Args...)> f) {}
};

int main() {
  Foo<int> m1(&Bar::MemberFunction);
  return 0;
}

This compiles fine. Now I want to write a factory function MakeFoo() which returns a unique_ptr to a Foo object:

template<typename ...Args>
std::unique_ptr<Foo<Args...>> MakeFoo(std::function<void(Bar*, Args...)> f) {
  return std::make_unique<Foo<Args...>>(f);
}

Using this function by calling

auto m2 = MakeFoo<int>(&Bar::MemberFunction);

in main, gives me the following compiler errors:

functional.cc: In function ‘int main()’:
functional.cc:21:50: error: no matching function for call to ‘MakeFoo(void (Bar::*)(int))’
       auto m2 = MakeFoo<int>(&Bar::MemberFunction);
                                                  ^
functional.cc:15:35: note: candidate: template<class ... Args> std::unique_ptr<Foo<Args ...> > MakeFoo(std::function<void(Bar*, Args ...)>)
     std::unique_ptr<Foo<Args...>> MakeFoo(std::function<void(Bar*, Args...)> f) {
                                   ^
functional.cc:15:35: note:   template argument deduction/substitution failed:
functional.cc:21:50: note:   mismatched types ‘std::function<void(Bar*, Args ...)>’ and ‘void (Bar::*)(int)’
       auto m2 = MakeFoo<int>(&Bar::MemberFunction);

It seems to me, that when I call the constructor of Foo, the compiler happily converts the function pointer &Bar::MemberFunction to a std::function object. But when I pass the same argument to the factory function, it complains. Moreover, this problem only seems to occur, when Foo and MakeFoo are variadic templates. For a fixed number of template parameters it works fine.

Can somebody explain this to me?

like image 716
Georg P. Avatar asked Sep 26 '17 15:09

Georg P.


1 Answers

Why doesn't it work without explicit <int>?

Prior to C++17, template type deduction is pure pattern matching.

std::function<void(Foo*)> can store a member function pointer of type void(Foo::*)(), but a void(Foo::*)() is not a std::function of any kind.

MakeFoo takes its argument, and pattern matches std::function<void(Bar*, Args...)>. As its argument is not a std::function, this pattern matching fails.

In your other case, you had fixed Args..., and all it had to do was convert to a std::function<void(Bar*, Args...)>. And there is no problem.

What can be converted to is different than what can be deduced. There are a myriad of types of std::function a given member function could be converted to. For example:

struct Foo {
  void set( double );
};
std::function< void(Foo*, int) > hello = &Foo::set;
std::function< void(Foo*, double) > or_this = &Foo::set;
std::function< void(Foo*, char) > why_not_this = &Foo::set;

In this case there is ambiguity; in the general case, the set of template arguments that could be used to construct some arbitrary template type from an argument requires inverting a turing-complete computation, which involves solving Halt.

Now, C++17 added deduction guides. They permit:

std::function f = &Foo::set;

and f deduces the signature for you.

In C++17, deduction doesn't guides don't kick in here; they may elsewhere, or later on.

Why doesn't it work with explicit <int>?

Because it still tries to pattern match and determine what the rest of Args... are.

If you changed MakeFoo to

template<class T>
std::unique_ptr<Foo<T>> MakeFoo(std::function<void(Bar*, T)> f) {
  return std::make_unique<Foo<T>>(f);
}

suddenly your code compiles. You pass it int, there is no deduction to do, and you win.

But when you have

template<class...Args>
std::unique_ptr<Foo<Args...>> MakeFoo(std::function<void(Bar*, Args...)> f) {
  return std::make_unique<Foo<T>>(f);
}

the compiler sees <int> and says "ok, so Args... starts with int. What comes next?".

And it tries to pattern match.

And it fails.

How can you fix it?

template<class T>struct tag_t{using type=T; constexpr tag_t(){}};
template<class T>using block_deduction=typename tag_t<T>::type;

template<class...Args>
std::unique_ptr<Foo<Args...>> MakeFoo(
  block_deduction<std::function<void(Bar*, Args...)>> f
) {
  return std::make_unique<Foo<T>>(f);
}

now I have told the compiler not to deduce using the first argument.

With nothing to deduce, it is satisfied that Args... is just int, and... it now works.

like image 194
Yakk - Adam Nevraumont Avatar answered Sep 29 '22 10:09

Yakk - Adam Nevraumont