2

Trying to pass a lambda function to a template factory function which is templated on the function arguments of the passed function leads gcc-10.2.0 to report no matching function for call to ‘test(main()::<lambda(int, double)>)’.

It does seem to work when I add a + in front of the lambda function forcing the conversion to a function pointer, but I don't see why that would be necessary. Why does the conversion not happen automatically? Is there any way to make this work?

I have also tried std::function<void(TArgs...)> test_func as argument in the declaration of make_test, however that gives me the same no matching function for call error.

#include <iostream>

template <typename... TArgs>
struct test_type {
  test_type(void(TArgs...)) {};
};

template <typename... TArgs>
test_type<TArgs...> make_test(void test_func(TArgs...)) {
  return test_type<TArgs...>{test_func};
}

int main() {
  auto test_object = make_test([](int a, double b) { std::cout << a << b << "\n"; });

  return 0;
}

Edit

I was wondering if there maybe is some way to make it work with type traits. Something like the following. Although I know of no way to get the argument list from the template parameter.


template <typename F>
test_type<get_arg_list<F>> make_test(std::function<F>&& f) {
  return test_type<get_arg_list<F>>{std::forward(f)};
}
wich
  • 15,075
  • 5
  • 42
  • 67
  • Is this helpful: https://stackoverflow.com/a/18889029/27678 – AndyG Apr 01 '21 at 19:45
  • 1
    @AndyG With or without `std::function`, I think OP is asking how to deduce the parameter types from the lambda. – HolyBlackCat Apr 01 '21 at 19:48
  • @RichardCritten because `auto test_object = test_type{[](int a, double b) { std::cout << a << b << "\n"; }}` works as expected. At least it does when I specify the constructor to take `std::function`, but the same cannot be said for the factory function for some reason. – wich Apr 01 '21 at 19:49

1 Answers1

1

In order to support a variety of callables being passed to your factory (e.g., a stateful lambda or a function pointer), your test_type constructor should accept some kind of type-erased function type like std::function<void(int, double)>:

template<class... TArgs>
struct test_type {
  test_type(std::function<void(TArgs...)>) {};
};

Afterwards it's just a matter of writing the boilerplate to handle the following callables being passed to make_test

  • a regular function pointer
  • a lambda (struct with a operator()(...) const)
  • a mutable lambda or a user defined callable without a const operator() function

Here is one approach using type traits:

Start with a base class that we'll specialize for each scenario:

template<class T, class = void>
struct infer_test_type;

(This is a common setup for the voider pattern. We can do this with concepts and constraints, but I'm feeling too lazy to look up the syntax, maybe later)

Regular function pointer specialization

template<class Ret, class... Args>
struct infer_test_type<Ret(*)(Args...)>
{
    using type = test_type<Args...>;
};

Now we can write a templated alias for simplicity:

template<class T>
using infer_test_type_t = typename infer_test_type<T>::type;

And we can verify that it works like so:

void foo(int, double){}
// ... 
static_assert(std::is_same_v<infer_test_type_t<decltype(&foo)>, test_type<int, double>>);

We can use the type trait for our make_test function like so:

template<class T>
auto make_test(T&& callable) -> infer_test_type_t<T>
{
    return infer_test_type_t<T>{std::forward<T>(callable)};
}

Now it's just a matter of covering our other two scenarios.

Callable objects

  • these will have operator() (either const or not)

I'll start with a top level trait to detect the presence of operator() and feed the type of operator() into another trait.

The top level trait:

// if T is a callable object
template<class T>
struct infer_test_type<T, std::void_t<decltype(&T::operator())>>
{
    using type = typename infer_test_type<decltype(&T::operator())>::type;
};

You see that internally it's calling back into another infer_test_type specialization that I haven't shown yet; one that is specialized for operator(). I'll show the two specializations now:

// if operator() is a const member function
template<class T, class Ret, class... Args>
struct infer_test_type<Ret(T::*)(Args...) const>
{
    using type = test_type<Args...>;
};

// if operator() is non-const member function
template<class T, class Ret, class... Args>
struct infer_test_type<Ret(T::*)(Args...)>
{
    using type = test_type<Args...>;
};

(We could probably combine these two if we wanted to be a little bit smarter and lop off any const at the high level before calling down, but I think this is more clear)

And now we should be able to infer an appropriate test_type for non-generic callables (no generic lambdas or templated operator() functions):

a test with a non-const operator():

struct my_callable
{
    void operator()(int, double) // non-const
    {
    }
};

// ...
static_assert(std::is_same_v<infer_test_type_t<my_callable>, test_type<int, double>>);

And a test with your lambda:

auto lambda = [](int a, double b) { std::cout << a << b << "\n"; };
static_assert(std::is_same_v<infer_test_type_t<decltype(lambda)>, test_type<int, double>>);

Putting it all together

For your simple (non-capturing, non-generic lambda) example it's quite straightforward:

make_test([](int a, double b) { std::cout << a << b << "\n"; });

Demo

AndyG
  • 35,661
  • 8
  • 94
  • 126
  • 1
    Deduction guide can also be added: `template test_type(T&&) -> infer_test_type_t;`. – Jarod42 Apr 02 '21 at 10:04
  • @Jarod42: Unfortunately the *template name* in the simple template id to the right of `->` in a deduction guide must match the *template name* of the class per [temp.deduct.guide], and I don't see a good workaround here on account of needing to infer variadic arguments. – AndyG Apr 02 '21 at 11:16
  • Indeed :-/ Sad... – Jarod42 Apr 02 '21 at 12:34
  • Thanks! That is a big mess for something seemingly so simple... You mention concepts and constraints, would that make this any nicer? – wich Apr 02 '21 at 19:49
  • @wich, unfortunately not much. We'd have to build a few things first. Reusable things, but if you aren't doing a lot of metaprogramming, then it probably wouldn't be worth it – AndyG Apr 02 '21 at 22:14