5

I'm trying to wrap my mind around the interaction of C++ lambda expressions and templates.

This code works as I expect:

#include <iostream>

int bar (int x, int (* f) (int))
{
    return f (x);
}

double bar (double x, double (* f) (double))
{
    return f (x);
}

int main ()
{
    std::cout << bar (16, [] (int x) -> int { return x * x; }) << std::endl;
    std::cout << bar (1.2, [] (double x) -> double { return x * x; }) << std::endl;

    return 0;
}

So does this:

#include <iostream>
#include <functional>

int bar (int x, std::function<int (int)> f)
{
    return f (x);
}

double bar (double x, std::function<double (double)> f)
{
    return f (x);
}

int main ()
{
    std::cout << bar (16, [] (int x) -> int { return x * x; }) << std::endl;
    std::cout << bar (1.2, [] (double x) -> double { return x * x; }) << std::endl;

    return 0;
}

So far, so good. But neither of the following examples compile:

#include <iostream>

template <typename T>
T bar (T x, T (* f) (T))
{
    return f (x);
}

int main ()
{
    std::cout << bar (16, [] (int x) -> int { return x * x; }) << std::endl;
    std::cout << bar (1.2, [] (double x) -> double { return x * x; }) << std::endl;

    return 0;
}

and

#include <iostream>
#include <functional>

template <typename T>
T bar (T x, std::function <T (T)> f)
{
    return f (x);
}

int main ()
{
    std::cout << bar (16, [] (int x) -> int { return x * x; }) << std::endl;
    std::cout << bar (1.2, [] (double x) -> double { return x * x; }) << std::endl;

    return 0;
}

GCC version 8.3.0 (with -std=c++17) gives an error message:

no matching function for call to 'bar(int, main()::<lambda(int)>' (and
another for the "double" version) and "template argument
deduction/substitution failed: main()::<lambda(int)> is not derived
from std::function<T(T)>" (for the second failing example).

However, this assignment works:

std::function<int (int)> f = [] (int x) -> int { return x * x; };

Could anyone please shed light for me? (Obviously, this isn't practical code. This is just an attempt to learn.)

Ken Ballou
  • 53
  • 3

4 Answers4

5

The problem is that implicit conversion (from lambda to function pointer or std::function) won't be considered in template argument deduction; then in your 3rd and 4th sample type deduction for T on the 2nd function argument (i.e. the lambda) fails.

Type deduction does not consider implicit conversions (other than type adjustments listed above): that's the job for overload resolution, which happens later.

In this case T could be deduced from only the 1st function argument; you can declare the 2nd one with non-deduced context, to exclude it from deduction.

e.g.

template <typename T>
T bar (T x, std::type_identity_t<T (*) (T)> f)
{
    return f (x);
}

template <typename T>
T bar (T x, std::type_identity_t<std::function <T (T)>> f)
{
    return f (x);
}

PS: std::type_identity is available from C++20, before that you can make one by your own, it's not difficult.

LIVE

songyuanyao
  • 147,421
  • 15
  • 261
  • 354
  • Thank you. I was unaware of std::type_identity_t being added in C++20. (I admit I have not yet studied C++20.) I'm afraid C++ is packing on weight faster that I am in pandemic quarantine! :-) – Ken Ballou May 08 '20 at 05:50
5

Your failing code is expecting function pointers during template argument deduction and lambdas are not function pointers.

However, non-capturing lambdas such as yours can be converted to function pointers.

The most terse way to do this is to apply a unary +. This works because a + is valid for pointers, triggering a conversion before template substitution occurs.

#include <iostream>

template <typename T>
T bar (T x, T (* f) (T))
{
    return f (x);
}

int main ()
{
    std::cout << bar (16, +[] (int x) -> int { return x * x; }) << std::endl;
//                        ^ this is the only change
    std::cout << bar (1.2, +[] (double x) -> double { return x * x; }) << std::endl;
//                         ^ this is the only change

    return 0;
}

See it compile on godbolt

Drew Dormann
  • 50,103
  • 11
  • 109
  • 162
  • Thank you. I have to admit that if I had seen this code with the unary plus in front of the lambda, I would have had no idea what was going on. – Ken Ballou May 08 '20 at 05:49
4

May be not what you exactly need but it works with C++11 (as Lambda functions are just syntactic sugar for anonymous functors):

#include <iostream>

template <typename T, typename L>
T bar (T x, const L& l)
{
    return l (x);
}

int main ()
{
    std::cout << bar (16, [] (int x) -> int { return x * x; }) << std::endl;
    std::cout << bar (1.2, [] (double x) -> double { return x * x; }) << std::endl;

    return 0;
}
abhiarora
  • 7,515
  • 3
  • 27
  • 46
2

Just in case, do not forget this works too:

template <typename T>
T foo(T (*f)(T))
{
  return 0;
}

template <typename T>
T bar(T x, T (*f)(T))
{
  return f(x);
}

int main()
{
  std::cout << bar<int>(16, [](int x) -> int { return x * x; }) << std::endl;
  std::cout << bar<double>(1.2, [](double x) -> double { return x * x; }) << std::endl;

  return 0;
}
Picaud Vincent
  • 8,544
  • 4
  • 19
  • 55
  • 1
    Yeah, that's, of course, one possible answer to the question "How to prevent issues in type deduction?" Don't use it. ;-) – Scheff's Cat May 06 '20 at 05:26