Templates[temp]

C++ generic lambdas, template lambdas and function objects

Abstract

A generic lambda (C++14) is an object whose call operator is a template; C++20 lets you write that template parameter list yourself, and C++23 adds deducing this for recursive lambdas and static call operators. Use a lambda for a local, one-off operation, a function template when callers overload it, name its types or find it by ADL, and a named function object when it must carry nested types such as is_transparent.

A line drawing of one star-shaped cookie cutter pressing the same star out of three strips of different material laid side by side: dough, clay and felt
Figure 1 — One call operator, stamped out again for each list of argument types.

A generic lambda is a lambda with auto in its parameter list, such as [](const auto& a, const auto& b) { return a < b; }. The compiler turns it into a class whose call operator is a template, so one object accepts any arguments its body can handle. Use one for a short operation written where it’s used. Write a template lambda (C++20) when the body needs the argument’s type by name, a function template when the operation is part of an interface that callers overload, call with explicit template arguments or find by ADL, and a named function object when it has to carry nested types such as is_transparent.

This guide is written for C++20. Generic lambdas arrived in C++14; C++20 added template parameter lists for lambdas and auto parameters in ordinary functions; C++23 added deducing this, which makes recursive lambdas straightforward, and static call operators. Every sample was compiled with MSVC 19.44 (/std:c++20 /EHsc /W4 /permissive-) and Clang 19.1 (-std=c++20 -Wall -Wextra, on Microsoft’s standard library) on one Windows x64 machine, without optimization unless the text says otherwise, and the C++23 samples with /std:c++23preview and -std=c++23; the results and errors shown are what those builds produced there.

What changed: a recursive lambda reaches itself through a std::function it captures by reference through an explicit object parameter, this const auto& self.

since C++23

Feature Standard GCC Clang MSVC
Generic lambdas (auto parameters) C++14 4.9 3.4 VS 2015
Template parameter lists for lambdas C++20 8 9 VS 2019 16.2
Abbreviated function templates (auto parameters in functions) C++20 10 10 VS 2019 16.9
Deducing this (explicit object parameters) C++23 14 18 VS 2022 17.13
static operator() C++23 13 16 VS 2022 17.14

Each cell is the first release the vendor lists for the feature, as collected in the C++14, C++20 and C++23 tables of Since When.

A generic lambda is a class with a template call operator

[Example 1:
auto less = [](const auto& a, const auto& b) { return a < b; };

bool r1 = less(1, 2.5);                 // T = int, U = double
bool r2 = less(std::string("a"), "b");  // std::string and char[2]

— end example]

For each auto parameter, the compiler adds an invented template parameter to the closure type’s call operator, in order of appearance (cppreference, Lambda expressions). The class it writes looks roughly like this:

[Example 2:
struct Less {
    template <class T, class U>
    auto operator()(const T& a, const U& b) const { return a < b; }
};

— end example]

Paste the lambda into C++ Insights to see the class the compiler generates for it. Three things follow from the picture:

  • One object, many functions. less is a single value, and every distinct list of argument types instantiates one more call operator.
  • Captures are members, exactly as for a lambda without auto.
  • It is the one local class allowed a member template. The standard says “A local class of non-closure type shall not have member templates” ([temp.mem]), so a struct with a template operator() inside a function body is an error (MSVC 19.44: “a template cannot be a member of a local class”), while a generic lambda in the same place is fine. Since C++14, a generic lambda is how you write a template inside a function.

auto parameters (C++14), and forwarding with auto&&

auto&& in a lambda’s parameter list is a forwarding reference, like T&& in a function template. It binds to anything, and decltype(s) records what it bound to: std::string& for an lvalue, std::string&& for an rvalue. Forward it with std::forward<decltype(s)>(s):

[Example 3:
std::vector<std::string> names;
auto add = [&names](auto&& s) {
    names.push_back(std::forward<decltype(s)>(s));
};

std::string first = "Ada";
add(first);               // lvalue: copied, first keeps "Ada"
add(std::string("Ben"));  // rvalue: moved
add("Cy");                // const char(&)[3]: converted

— end example]

Move semantics: what std::move and && really do explains what the cast inside std::forward does. A pack works the same way, auto&&... args, and decltype(auto) as the return type keeps a reference when the wrapped call returns one, where plain auto would return a copy:

[Example 4:
auto logged = [](auto&& f, auto&&... args) -> decltype(auto) {
    std::puts("calling");
    return std::forward<decltype(f)>(f)(
        std::forward<decltype(args)>(args)...);
};

— end example]

Each auto is a separate template parameter, so [](auto a, auto b) takes an int and a double in one call. When the parameters must agree, or the body needs the type’s name, C++14 leaves you with decltype and type traits. C++20 lets you write the template head.

Template parameter lists for lambdas (C++20)

[Example 5:
// C++20: name the element type in the parameter list.
auto sum = []<class T>(const std::vector<T>& v) {
    T total{};
    for (const T& x : v) total += x;
    return total;
};

// C++14: recover it from the argument instead.
auto sum14 = [](const auto& v) {
    using T = typename std::decay_t<decltype(v)>::value_type;
    T total{};
    for (const T& x : v) total += x;
    return total;
};

— end example]

A template parameter list after the captures gives a lambda what a function template’s head gives a function:

  • A pattern to match. const std::vector<T>& accepts vectors only, and names their element type.
  • One type for two parameters. same below rejects same(1, 2.0); Clang 19 reports “deduced conflicting types for parameter ‘T’ (‘int’ vs. ‘double’)”.
  • Packs you forward by name: std::forward<Ts>(args)... instead of std::forward<decltype(args)>(args)....
  • Non-type parameters, such as a std::size_t N.
  • Constraints, on the template parameter or on auto.
[Example 6:
auto same = []<class T>(const T& a, const T& b) { return a == b; };

auto make = []<class... Ts>(Ts&&... args) {
    return std::vector<std::string>{std::forward<Ts>(args)...};
};

auto nth = []<std::size_t N>(const auto& tuple) {
    return std::get<N>(tuple);
};
char c = nth.operator()<1>(std::tuple{1, 'x'});  // 'x'

auto half = []<std::integral T>(T x) { return x / 2; };
auto half2 = [](std::integral auto x) { return x / 2; };

— end example]

nth shows the cost. A lambda’s template arguments can only be given by naming its call operator, and inside a template you also need the template keyword, nth.template operator()<1>(t): Clang 19 rejects the call without it, MSVC 19.44 accepts both. When callers have to name types, a function template reads better. A written list and auto parameters mix, and the invented parameters go after the written ones. std::integral auto is a constrained auto: the invented template parameter must satisfy std::integral. C++20 concepts: how to write and use them covers what can stand in front of auto and how to write your own concepts.

auto parameters in ordinary functions (C++20)

C++20 allows auto parameters in ordinary function declarations too (not in a virtual function, since a member template can’t be virtual). A function with an auto parameter is an abbreviated function template, with one invented template parameter per auto parameter, appended to any you wrote:

[Example 7:
// C++20: each auto parameter adds a template parameter.
void print_all(const auto&... xs) {
    ((std::cout << xs << ' '), ...);
    std::cout << '\n';
}
// The same as:
// template <class... Ts> void print_all(const Ts&... xs);

auto twice(std::integral auto x) { return 2 * x; }

// Written template parameters come first, invented ones after them.
template <class To>
To convert(const auto& from) { return static_cast<To>(from); }

int x = convert<int>(2.75);  // To = int; from is a const double&

— end example]

It is still a template, with everything that implies: the definition must be visible where it’s called (in practice, in a header), it can be explicitly specialized, and an error in its body appears when it’s instantiated. A constraint moves the error to the call. For twice(2.5), Clang 19 says the candidate was ignored “because ‘double’ does not satisfy ‘integral’”, and MSVC 19.44 that the concept 'std::integral<double>' evaluated to false.

What you get back differs from a generic lambda: a named function that you can overload, declare, call with explicit arguments (convert<int>(2.75) returns 2) and find through ADL, rather than an object. That is also why twice can’t be passed to an algorithm by name.

Passing a function template or an overload set to an algorithm

An algorithm such as std::transform deduces the type of its function parameter from the argument. The name of a function template, or of an overloaded function, has no single type to deduce, so the call doesn’t compile:

[Example 8:
template <class T>
T square(T x) { return x * x; }

int half(int x) { return x / 2; }
double half(double x) { return x / 2; }

// std::transform(b, e, out.begin(), square);  // error: no T
std::transform(b, e, out.begin(), square<int>);  // OK, fixed T
std::transform(b, e, out.begin(),
               [](auto x) { return square(x); });  // OK

// std::transform(b, e, out.begin(), half);  // error: which half?
std::transform(b, e, out.begin(),
               [](auto x) { return half(x); });  // OK

— end example]

For both commented-out calls, MSVC 19.44 says it “could not deduce template argument for ‘_Fn’”, and Clang 19 that it “couldn’t infer template argument ‘_Fn’”. There are two fixes:

  1. Give the argument a type. Name one specialization (square<int>), or cast to the exact pointer type (static_cast<int (*)(int)>(half)). Both fix the type at the call site.
  2. Pass an object instead. A lambda that makes the call moves overload resolution into its body, where it happens with the real argument each time. The general form forwards everything and keeps the result as it is:
[Example 9:
auto half_fn = [](auto&&... args) -> decltype(auto) {
    return half(std::forward<decltype(args)>(args)...);
};

— end example]

Use the lambda for standard library functions too. Since C++20, the standard makes it unspecified, possibly ill-formed, to form a pointer or reference to a standard library function unless it is designated addressable ([namespace.std]), and the list of those is short, mostly I/O manipulators such as std::endl. The <cctype> functions add a trap of their own: their behavior is undefined for any value that is neither representable as unsigned char nor EOF, which includes negative char values, so convert first:

[Example 10:
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
    return static_cast<char>(std::toupper(c));
});

— end example]

Named function objects: transparent comparators and the ranges algorithms

A named function object is a class with an operator(), which is all a lambda’s closure type is, plus a name and anything else you declare in the class. The C++ Core Guidelines’ rule T.40 prefers function objects for passing operations to algorithms because they “can carry more information through an interface” than a plain pointer to function. Two parts of the standard library show what that information buys.

Transparent comparators

std::less<>, the void specialization added in C++14, deduces both argument types and declares a nested type, is_transparent. An ordered container whose comparator has that member gets template overloads of its lookup functions (find, count, lower_bound and the rest, and C++20’s contains), so a key of another type is compared as it is, without building a std::string first:

[Example 11:
std::set<std::string, std::less<>> words{"alpha", "beta"};
std::string_view key = "beta";
bool found = words.find(key) != words.end();

std::map<std::string, int, std::less<>> stock{{"bolts", 40}};
bool has_bolts = stock.contains("bolts");  // contains(): C++20

— end example]

With the default std::set<std::string>, words.find(key) doesn’t compile, because std::string’s constructor from a std::string_view is explicit. A lambda can’t declare is_transparent, since you can’t add members to a closure type. For a heterogeneous comparator of your own, write a struct with using is_transparent = void; and an operator() that accepts every key type you look up with. How to write comparators for std::sort and std::map covers the rules every comparator must follow, and shows a captureless lambda used as a set’s comparator type, which works since C++20 made such lambdas default-constructible.

The ranges algorithms are objects

Since C++20, std::ranges::sort and the other constrained algorithms in std::ranges are algorithm function objects, not function templates. You can’t give them explicit template arguments, argument-dependent lookup doesn’t find them, and when ordinary unqualified lookup finds one, ADL is switched off for that call. And because they are objects, they can be passed:

[Example 12:
struct Person {
    std::string name;
    int age;
};

std::vector<std::vector<int>> rows{{3, 1, 2}, {9, 7, 8}};
std::ranges::for_each(rows, std::ranges::sort);  // sorts each row
// std::ranges::for_each(rows, std::sort);  // error: a template

std::vector<Person> people{{"Ada", 36}, {"Ben", 29}};
std::ranges::sort(people, {}, &Person::age);  // by age, default <

— end example]

The projection, &Person::age, is the same idea from the other side: the ranges algorithms accept anything std::invoke can call, a pointer to member included, and apply it to each element before comparing.

Recursive lambdas with deducing this (C++23)

A lambda can’t call itself by name. In auto fact = [](int n) -> int { ... fact(n - 1) ... };, Clang 19 reports that a “variable ‘fact’ declared with deduced type ‘auto’ cannot appear in its own initializer”. C++23’s explicit object parameter, from P0847R7, Deducing this, solves it: the first parameter, marked this, is the closure object itself.

[Example 13:
struct Node {
    int value;
    std::vector<Node> children;
};

Node tree{1, {Node{2, {}}, Node{3, {Node{4, {}}}}}};

// C++23: the lambda receives itself as an explicit object parameter.
auto total = [](this const auto& self, const Node& n) -> int {
    int sum = n.value;
    for (const Node& c : n.children) sum += self(c);
    return sum;
};

— end example]

total(tree) returns 10. Before C++23 there were two workarounds, and both return 10 here too:

[Example 14:
// C++14: pass the lambda to itself.
auto total14 = [](const auto& self, const Node& n) -> int {
    int sum = n.value;
    for (const Node& c : n.children) sum += self(self, c);
    return sum;
};
int t14 = total14(total14, tree);

// C++11: a std::function that the lambda captures by reference.
std::function<int(const Node&)> total11 = [&](const Node& n) {
    int sum = n.value;
    for (const Node& c : n.children) sum += total11(c);
    return sum;
};

— end example]

The std::function version fixes the signature, so it can’t be generic, and it refers to total11 by reference: a copy that outlives total11 calls an object that no longer exists. Three rules apply to the C++23 form, each checked on both compilers here:

  1. Write the return type, or put a non-recursive return before the first recursive call. Without -> int, self(c) needs a return type that hasn’t been deduced yet, and MSVC 19.44 reports that “a function that returns ‘auto’ cannot be used before it is defined”.
  2. Captures are allowed, but the explicit object parameter must then be the closure type, a class derived from it, or a reference to one of those. A deduced this auto&& self qualifies, so [&visited](this auto&& self, const Node& n) -> void can walk the tree and count nodes into a local variable.
  3. No conversion to a function pointer. A lambda with an explicit object parameter has none, even when it captures nothing.

One portability note: neither compiler here defines the feature-test macro __cpp_explicit_this_parameter, under /std:c++23preview, /std:c++latest, -std=c++23 or -std=c++2c, although both compile every sample above. Code guarded by #ifdef __cpp_explicit_this_parameter takes the fallback path on both.

static call operators (C++23)

A lambda that captures nothing can declare its call operator static, as long as it isn’t also mutable and has no explicit object parameter. The operator then needs no object, and the conversion to a function pointer points straight at it:

[Example 15:
auto by_abs = [](int a, int b) static {
    return std::abs(a) < std::abs(b);
};
std::vector<int> v{-3, 1, -2};
std::ranges::sort(v, by_abs);  // 1, -2, -3
bool (*fp)(int, int) = by_abs;  // points at the static operator()
bool direct = decltype(by_abs)::operator()(1, -2);  // no object

— end example]

A named function object can declare static bool operator()(int a, int b) the same way, also since C++23.

Which to use: generic lambda, function template or function object

Generic or template lambda Function template Named function object
Since C++14 (auto), C++20 (template list) C++98; auto parameters C++20 C++98
Written inside a function body Yes No Yes, with no template operator()
Captures local variables Yes No Only through members you initialize
Passed to an algorithm by name Yes: it is an object No: name a specialization or wrap it Yes: it is an object
Several overloads under one name No Yes Yes: several operator()
Explicit template arguments Only as f.operator()<T>(x) f<T>(x) Only as f.operator()<T>(x)
Found by ADL No Yes No
Nested types such as is_transparent No Not applicable Yes
  • Generic lambda: a short operation used in one place, as an algorithm argument or a callback, with auto&& when it forwards.
  • Template lambda (C++20): the same, when the body needs a type’s name, two parameters must share a type, or a parameter is a pattern such as std::vector<T>.
  • Function template, or its abbreviated form: an operation that belongs to an interface, which callers overload, call with explicit template arguments or find by ADL. Wrap it in a lambda where you pass it to an algorithm.
  • Named function object: an operation that needs nested types (is_transparent), several call operators or a type with a name of its own, such as a comparator that a code base uses as a container parameter in many places. Declare its operator() static (C++23) when it holds no state.

To try a sample on other compilers, or see the closure class, Compiler Explorer and C++ Insights are both on Toolchain. More generic code, from function templates to concepts, is in Templates.

Questions

What is the difference between a generic lambda and a function template?

A generic lambda is an object of a closure class whose call operator is a template; a function template is a family of functions. The lambda can be written inside a function body, capture local variables and be passed to an algorithm as it is. The function template can be overloaded, found by argument-dependent lookup and called with explicit template arguments such as f<int>(x).

How do I write a recursive lambda?

In C++23, give it an explicit object parameter, [](this const auto& self, int n) -> int, and call self(...) inside. Before C++23, pass the lambda to itself as an extra parameter (C++14), or store it in a std::function that it captures by reference (C++11), which fixes the signature and dangles if the std::function is destroyed first. Write the return type, because the recursive call comes before a return statement has deduced it.

Can a generic lambda be converted to a function pointer?

Yes, if it captures nothing: it has a conversion function template, so int (*p)(int) = [](auto x) { return x; }; instantiates the call operator for int and points at it. A lambda that captures, or one with a C++23 explicit object parameter, has no conversion to a function pointer.

How do I pass an overloaded function to std::transform?

Wrap the call in a lambda, such as [](auto x) { return half(x); }, so that overload resolution happens inside the body with the real argument. A static_cast to the exact function pointer type also works for your own functions. For standard library functions use the lambda, because since C++20 taking their address is unspecified unless the function is designated addressable.

Can two auto parameters be forced to have the same type?

Not with plain auto, since each auto parameter gets its own invented template parameter. Write a template parameter list instead (C++20): []<class T>(const T& a, const T& b) rejects a call with an int and a double. To deduce from the first argument and convert the second, declare the second as std::type_identity_t<T>.

Read next

  1. C++ expression templates explained with a C++20 exampleTemplates · Miriam Castellanos · written for C++20
  2. C++20 concepts: how to write and use themStandards · Miriam Castellanos · written for C++20
  3. Strings as C++ template parameters with C++20 fixed_stringTemplates · Miriam Castellanos · written for C++20