How to transfer to a template with a parameter-function lambda? Why doesn't it work like that?

The minimum example is:

template <typename T> void invoke(std::function<void(T)> f, T val) { f(val); } int main() { auto printer = [](int x){ std::cout << x; }; ::invoke(printer, 42); } 

Of course, I can explicitly specify ::invoke<int> or specify an int function parameter, but then it must be done with each call and there is no point in the template ... How to make, in general, the call ::invoke(...) have worked?

PS: just change std::function to a template parameter is impossible (because you need to redo a bunch of code then)

  • It does not work, because from the anonymous_class type being transferred it is not possible to display the template parameter T It is not clear why you need this function if there is std::invoke . - VTT

4 answers 4

If the template parameter appears in the type of some parameter of the function and is located there in the so-called. deduced context , the corresponding template argument will be deduced by the compiler from the type of the corresponding function argument. It requires that:

  1. all deductions of such template arguments (in each parameter of the function) were successful, and
  2. all deductions of the same patterned argument gave the same result

That is, here is an example

 template <typename T> struct S { S(T) {} }; template <typename T> void foo(T t, S<T> s) {} int main() { foo(5, 5); } 

will not compile because deduction of the template argument T from the type of the second argument of the function fails. The fact that the template argument T successfully deduced from the type of the first argument of the function does not save the situation. In this example, T is in a deductible context in both the first and second parameters. This means that deduction must be performed through both parameters and must complete successfully in both (and must output the same T value). Otherwise, the code is incorrect.

It is possible to suppress this destructive behavior of the second parameter of the function if, in the declaration of this parameter, you intentionally place T in the non-deduced context. For example, if the type of the second parameter is described as a nested type of a foreign wrapper pattern

 template <typename T> struct S { S(T) {} }; template <typename T> struct W { using S = ::S<T>; }; template <typename T> void bar(T t, typename W<T>::S s) {} int main() { bar(5, 5); } 

In this variant, the second argument of the function is excluded from the deduction process T and the deduction T is performed only on the basis of the first argument (and is successful).

Your example suffers from the same problem. That is, in your example, a working “crutch” might look like

 template <typename T> struct W { using F = std::function<void(T)>; }; template <typename T> void invoke(typename W<T>::F f, T val) { f(val); } int main() { auto printer = [](int x){ std::cout << x; }; ::invoke(printer, 42); } 

It may well be that I am missing the already prepared standard tool for solving the same problem in the same way.

  • In order not to make a separate wrapper class for each such case, you can use std::enable_if_t<1, std::function<void(T)>> . Another option: std::common_type_t<std::function<void(T)>> . - HolyBlackCat
  • one
    @HolyBlackCat: I'm just looking for some kind of std::identity_wrapper or something like that. Or better std::non_deduced . The variants with std::enable_if , etc., of course, work, but at first glance they are confusing. - AnT 5:42
  • That is, the order of the arguments plays a role? If you declare foo(T t, S<T> s) will output? - Cerbo
  • @Cerbo: No, it does not. I wrote: it is required that all deductions of patterned arguments (in each parameter of the function) be successful. All The order is not important. In my example, it is impossible to deduce T from argument 5 for a parameter of type S<T> . It will always “fall”, where in the list of parameters this S<T> not located. - AnT 7:26

You can make a variadic template, then you do not have to change the already existing code:

 template <typename function_t, typename... args_t> decltype(auto) invoke_args(function_t _function, args_t&&... _args) { return (_function)(std::forward<args_t>(_args)...); } void f_printer(int x) { std::cout << "f_" << x << std::endl; } int main() { auto printer = []() { std::cout << "empty" << std::endl; }; auto printer1 = [](int x) { std::cout << x << std::endl; }; auto printer2 = [](int x, int y) { std::cout << x << " " << y << std::endl; }; invoke_args(f_printer, 42); // f_42 invoke_args(printer); // empty invoke_args(printer1, 42); // 42 invoke_args(printer2, 42, 84); // 42 84 system("pause"); } 

Such a template will work with a variable number of arguments and will return a type in accordance with the return type of the function.

    In addition to the @AnT answer .

    In C++20 , the <type_traits> std :: type_identity should appear in <type_traits> , with which you can do the same thing that @AnT describes in the response:

     #include <type_traits> template <typename T> void invoke(std::type_identity_t<std::function<void(T)>> f, T val) { f(val); } int main() { auto printer = [](int x){ std::cout << x; }; ::invoke(printer, 42); } 

    Original proposal ; revision of the original proposal , approved in June 2018.

      As already indicated in the commentary question, the problem is that it is impossible to deduce type T from lambda when instantiating a template.

      I see at least two possible solutions:

      • Replace auto with a specific type of std::function<void(int)> functor.
      • Add a template version with two parameters:

         template <typename T, typename F> void invoke(F f, T val) { f(val); } 

        Then the lambda will be recognized as type F in the new implementation.