Interaction of C++ lambdas and templates

Viewed 95

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.)

4 Answers

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

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

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

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