Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cast lambda to std::function with parameter pack

There are several questions on SO that relate to casting lambdas to std::functions, but I have yet to see one that uses a parameter pack for the argument list. This seems broken on my version of g++ (7.1.1-4), and possibly it's just not supported. So is this legal c++17 (by the standard)? If not, why?

#include <functional>

template <typename TReturn, typename ... TArgs>
void Functor(std::function<TReturn (TArgs...)> f) {}

int main(int argc, char * argv[]) {
    auto x = [] (int a, int b) { return a * b; };
    Functor<int, int, int>(x);
    return 0;
}

The code above won't compile because it fails type deduction. Obviously explicitly typing x as std::function<int (int, int)> instead of using auto makes the error go away. But that doesn't allow me to pass an r-value to Functor as I would like. I would also like to not loose any type-safety by using another template parameter for the function type.

What I really don't understand is why the above code fails to compile, but the below code is fine and works:

#include <functional>

template <typename TReturn, typename TArgA, typename TArgB>
void Functor(std::function<TReturn (TArgA, TArgB)> f) {}

int main(int argc, char * argv[]) {
    auto x = [] (int a, int b) { return  a * b; };
    Functor<int, int, int> (x);
    return 0;
}
like image 826
Howard Avatar asked Jul 21 '17 19:07

Howard


3 Answers

The issue is that the compiler doesn't know that you've intended int, int to be the whole of TArgs, and so tries to deduce the remainder of TArgs from the argument f.

For example, this would be valid:

Functor<int, int, int>(std::function<int(int, int, char, float)>{});
// TArgs := {int, int, [...]                       char, float}

So you need to instruct the compiler to not try to deduce the remainder of TArgs. For example, you could write:

(*Functor<int, int, int>)(x);

Or you could write Functor with a non-decomposed signature Sig:

template <Sig>
void Functor(std::function<Sig> f) {}

Or you could wrap the use of TArgs in the parameter f in a non-deduced context:

template <typename TReturn, typename ... TArgs>
void Functor(std::function<std::conditional_t<false, void, TReturn (TArgs...)>> f) {}
like image 164
ecatmur Avatar answered Oct 16 '22 18:10

ecatmur


This fails:

#include <functional>

template <typename TReturn, typename ... TArgs>
void Functor(std::function<TReturn (TArgs...)> f) {}

int main(int argc, char * argv[]) {
    auto x = [] (int a, int b) { return a * b; };
    Functor<int, int, int>(x);
    return 0;
}

because you're not specifying that the entirety of TArgs... is {int, int}. What you are doing is specifying that the first two types are {int, int}. Effectively, by providing those three types, we've turned the deduction problem into:

template <typename ... TArgs>
void Functor(std::function<int(int, int, TArgs...)> f) {}

int main(int argc, char * argv[]) {
    auto x = [] (int a, int b) { return a * b; };
    Functor(x);
    return 0;
}

This doesn't compile because a lambda isn't a std::function (or derived from one), which is the same reason you couldn't have called this without having provided any types to begin with.

The non-variadic version doesn't have this problem, since you've provided all the types.


But really, what you want is:

template <typename F>
void Functor(F ) {}

This doesn't lose you any type safety. It's using std::function that loses type information, since that class template exists to type erase.

like image 33
Barry Avatar answered Oct 16 '22 18:10

Barry


It is very rarely a good idea to cast a lambda to a std::function in a template if you are just going to call it. std::function is type-erasure, and templated type erasure only makes sense if you are going to "pass it on" somewhere else and/or return it.

In any case, try this:

template <class Sig>
void Functor(std::function<Sig> f) {}

int main(int argc, char * argv[]) {
  auto x = [] (int a, int b) { return a * b; };
  Functor<int(int, int)>(x);
  return 0;
}

but you should really just do

template <class F>
void Functor(F f) {}

which is perfectly type-safe.

If you want early type checking, you could write

template<class Sig, class F>
struct signature_compatible;
template<class R, class...Args, class F>
struct signature_compatible<R(Args...), F> :
  std::is_consructible< R, std::result_of_t<F(Args...)>>
{};

then do

template <class Sig, class F>
void Functor(F f) {
  static_assert( signature_compatible<Sig, F&>::value, "bad signature" );
}

but only if you really need to.

like image 2
Yakk - Adam Nevraumont Avatar answered Oct 16 '22 18:10

Yakk - Adam Nevraumont