Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 3 of 5

A lambda with an auto parameter works for any type that fits the body:

auto print = [](const auto& x) { std::cout << x << " "; };

That is C++14, and it is a template written in four fewer lines than a template.

What auto parameters buy

#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>

int main() {
    // C++14: auto parameters make operator() a template.
    auto print = [](const auto& x) { std::cout << x << " "; };
    print(42); print(3.5); print(std::string("text")); print('c');
    std::cout << "\n";

    // Deducing several parameters independently.
    auto add = [](auto a, auto b) { return a + b; };
    std::cout << add(1, 2) << " " << add(1.5, 2.5) << " "
              << add(std::string("a"), "b") << "\n";

    // The real payoff: iterating a map without naming the pair type.
    std::map<std::string, std::vector<int>> data = {
        {"alpha", {1, 2}}, {"beta", {3, 4, 5}}
    };
    std::for_each(data.begin(), data.end(), [](const auto& entry) {
        std::cout << entry.first << " has " << entry.second.size() << " items\n";
    });

    // C++20: an explicit template parameter list, when auto is not enough.
    auto first_two = []<typename T>(const std::vector<T>& v) -> std::pair<T, T> {
        return {v[0], v[1]};
    };
    auto p = first_two(std::vector<int>{7, 8, 9});
    std::cout << "first two: " << p.first << " " << p.second << "\n";

    // Variadic generic lambda: perfect forwarding in a closure.
    auto call_with = [](auto&& f, auto&&... args) {
        return f(std::forward<decltype(args)>(args)...);
    };
    std::cout << "forwarded: " << call_with(add, 10, 20) << "\n";
    return 0;
}
42 3.5 text c 
3 4 ab
alpha has 2 items
beta has 3 items
first two: 7 8
forwarded: 30

The map iteration is where this earns its keep. Without auto, that parameter would have to be spelled const std::pair<const std::string, std::vector<int>>& — note the const on the key (lesson 13.4), which is the part people get wrong and then spend ten minutes debugging.

add(std::string("a"), "b") returns "ab" — the two parameters deduce independently, so auto a, auto b is two template parameters, not one. add(1, 2.5) would also work, which the single-parameter maximum of lesson 12.2 could not.

The C++20 explicit form []<typename T>(const std::vector<T>& v) names the type so you can use it — here as the return type. auto alone cannot express "a vector of T, returning a pair of T".

auto&&... plus std::forward gives a closure that forwards perfectly (lesson 12.6). Note std::forward<decltype(args)>(args)... rather than std::forward<Args> — inside a lambda there is no Args to name, so decltype supplies it.

What it compiles to

Lesson 14.1 established that a lambda is a struct with operator(). A generic lambda is a struct with a template operator():

// auto add = [](auto a, auto b) { return a + b; };
//
// is roughly:
//
// struct AddClosure {
//     template <typename T, typename U>
//     auto operator()(T a, U b) const { return a + b; }
// };

One consequence catches people out: a generic lambda is one object, not several. The output above shows sizeof(generic) is 1 even though it was called with an int and a std::string — the closure has no members, and the instantiations are of its operator(), not of the closure type.

That also means a generic lambda can be stored in one variable and used with many types, which a std::function cannot do (lesson 14.4) — std::function<int(int)> fixes the signature.

Constraining them

An unconstrained auto accepts anything, and fails deep inside the body when the type does not fit — exactly the problem lesson 12.7 solved for templates. Concepts apply to lambdas the same way:

#include <concepts>
#include <iostream>
#include <string>
#include <vector>

int main() {
    // A generic lambda accepts anything — including things it should not.
    auto unconstrained = [](const auto& a, const auto& b) { return a + b; };
    std::cout << unconstrained(1, 2) << "\n";

    // Constrained with a concept (C++20): the error moves to the call site.
    auto numeric_add = []<typename T>(T a, T b) requires std::integral<T> || std::floating_point<T> {
        return a + b;
    };
    std::cout << numeric_add(3, 4) << " " << numeric_add(1.5, 2.5) << "\n";

    // The abbreviated form reads better for simple constraints.
    auto only_ints = [](std::integral auto a, std::integral auto b) { return a * b; };
    std::cout << only_ints(6, 7) << "\n";

    // A generic lambda is one closure type with a TEMPLATE operator(),
    // not several closure types.
    auto generic = [](auto x) { return x; };
    std::cout << "sizeof: " << sizeof(generic) << "\n";
    std::cout << generic(1) << " " << generic(std::string("s")) << "\n";
    return 0;
}
3
7 4
42
sizeof: 1
1 s

[](std::integral auto a, std::integral auto b) is the form to reach for. It reads as a signature, it rejects a std::string at the call site with a message naming std::integral, and it costs nothing.

Note numeric_add(1.5, 2.5) printing 4 rather than 4.0 — stream formatting again (lesson 6.7), not truncation. The requires clause admitted the double correctly.

auto versus const auto&

The same decision as any parameter (lesson 6.4), and it matters more here because you do not know the type:

// [](auto x)        — copies. Fine for int and double, expensive for a vector.
// [](const auto& x) — no copy. The right default for a generic lambda.
// [](auto& x)       — modifies the caller's object.
// [](auto&& x)      — forwarding reference; use with std::forward.

Default to const auto&. A generic lambda might be handed a std::vector of a million elements, and auto x would copy it on every call.

Where they matter most

Iterating associative containers, as above — the element type is awkward to spell.

Comparators over any record type:

// auto by_score = [](const auto& a, const auto& b) { return a.score > b.score; };
// std::sort(players.begin(), players.end(), by_score);
// std::sort(teams.begin(), teams.end(), by_score);      // works for both

One comparator, two unrelated types, because it asks only for a .score. That is duck typing with compile-time checking.

Visiting a std::variant:

// std::visit([](const auto& value) { std::cout << value; }, v);

One generic lambda handles every alternative — provided they all support the operation. When they do not, you need one handler per type, which is the overloaded idiom from lesson 13.10.

Try it

Write a generic print_container lambda that works for vector, list, set, and map — the map printing key=value and the others printing elements. Then constrain a sum_all lambda to numeric element types and show what the error looks like for a container of strings.

Checkpoint

  • An auto parameter (C++14) makes a lambda generic — operator() becomes a template.
  • A generic lambda is one closure object with a template operator(), not several closures.
  • auto a, auto b are two independent template parameters, so mixed types work.
  • Its main everyday use is iterating maps without spelling std::pair<const K, V>.
  • C++20's []<typename T>(...) names the type, which auto alone cannot.
  • auto&&... with std::forward<decltype(args)>(args)... forwards perfectly inside a closure.
  • Default to const auto& — you do not know how expensive the type is to copy.
  • Constrain with std::integral auto x or a requires clause to move errors to the call site.
  • if constexpr (requires { ... }) tests a property at compile time and discards the other branch.
  • One generic comparator can sort unrelated types that share a field name.

Questions about this lesson

Log in to ask a question.