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
autoparameter (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 bare 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, whichautoalone cannot. auto&&...withstd::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 xor arequiresclause 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.