Howard
Howard

Reputation: 218

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;
}

Upvotes: 10

Views: 978

Answers (4)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275200

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.

Upvotes: 2

bolov
bolov

Reputation: 75668

Here is a solution that will let you call functor without specifying it's template argument:

#include <functional>
#include <type_traits>

template <class T> struct Fun_trait {};

template <class T, class... Args, class Ret>
struct Fun_trait<auto (T::*) (Args...) const -> Ret>
{
    using F = std::function<auto (Args...) -> Ret>;
};


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

template <class F>
std::void_t<decltype(&F::operator())>
functor(F f)
{
    return functor<typename Fun_trait<decltype(&F::operator())>::F>(f);
};


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

    // nice and easy:
    functor(x); 

    return 0;
}

This is just a lazy first draft to get you started. You need to expand it to support forwarding and non-const operator().


It works in 2 stages:

1st we have Fun_trait who - for pointer method types (e.g. the operator() of a lambda) - has defined an alias F for the required std::function argument type.

Next we have a overload of your function functor which via SFINAE with std::void_t kicks in only for functors with a non-overloaded operator() (e.g. a lambda) and then using the above trait calls the main function functor with the correct template argument deduced.

Upvotes: 0

Barry
Barry

Reputation: 302643

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.

Upvotes: 6

ecatmur
ecatmur

Reputation: 157314

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) {}

Upvotes: 12

Related Questions