[2026] C++ Generic Lambdas — auto Parameters and Template Lambdas (C++20)
이 글의 핵심
This guide covers plain vs generic lambdas, how C++14 auto parameters turn operator() into a function template, C++20 template lambdas, practical STL patterns, type deduction rules, and performance considerations in one place.
What is a generic lambda?
A generic lambda is a C++14 feature: you put auto on a parameter so the same body can be reused for several types. Internally, the closure type’s operator() is a function template.
auto add = [](auto a, auto b) { return a + b; };
int x = add(1, 2); // int
double y = add(1.5, 2.5); // double
Why use it?
- Less duplication: you do not need separate lambdas for
intanddouble. - STL-friendly: easy to pass one comparison or predicate into
std::sort,std::find_if, and similar. - C++20 onward: pairs naturally with the more explicit template lambda syntax.
Reading Lambda basics and auto type deduction first will make this article easier to follow.
Plain lambda vs generic lambda
Plain lambda (non-template operator())
If parameter types are fixed, the closure’s operator() is a single ordinary member function.
auto cmp = [](int a, int b) { return a < b; };
// Conceptually: struct __lambda { bool operator()(int a, int b) const; };
It fits one type; to use double or std::string you would need another lambda.
Generic lambda (auto parameters)
Using auto (or auto&, const auto&, etc.) on a parameter makes operator() a function template.
auto cmp = [](auto a, auto b) { return a < b; };
// Conceptually: struct __lambda {
// template<typename T, typename U>
// auto operator()(T a, U b) const { return a < b; }
// };
The same lambda object can sort and compare int, double, and user-defined types that define operator<.
| Aspect | Plain lambda | Generic lambda |
|---|---|---|
| Parameters | Concrete types | auto / decltype(auto), etc. |
operator() | Non-template | Function template |
| Instances | One | Specializations as needed per call |
| Readability | Clear when simple | Signals “multiple types allowed” |
How auto parameters work
The C++14 rules boil down to this:
- For each
autoparameter in a generic lambda, a distinct template type parameter is introduced. - If the lambda’s return type is not specified, return type deduction follows rules similar to
decltype(auto)from the body’sreturnstatements (same idea as for ordinary lambdas).
[](auto x) { return x * 2; } // A template instance per type of x
[](auto& x) { return x; } // Reference: no copy for read/write
Caveat: plain auto is by value. For large objects, prefer const auto& or auto&& (forwarding-reference-like deduction). That lines up with perfect forwarding.
// Runnable example
std::vector<std::string> v = {"a", "b"};
std::for_each(v.begin(), v.end(), [](const auto& s) {
std::cout << s << '\n'; // Avoid unnecessary copies
});
The compiler’s closure type is implementation-specific, but the key point is that operator() is a template. Template instantiations appear per call pattern, and like ordinary function templates they can be duplicated across TUs that are not merged—same trade-offs as templates generally.
C++20 template lambdas
C++20 lets you express the same idea with explicit template syntax.
auto f = []<typename T>(T x) { return x + x; }; // One type parameter
auto g = []<class T, class U>(T a, U b) { return a < b; };
Differences and benefits
- Named type parameters:
Tis visible in the signature. - Easier to attach
requiresconstraints (C++20 concepts) directly on the lambda.
auto clamp_positive = []<typename T>(T x) requires std::is_arithmetic_v<T> {
return x > T{0} ? x : T{0};
};
With auto parameters only, constraining the anonymous template parameters is awkward; template lambdas pair well with requires. For syntax details see C++ template lambdas and generic lambdas and error messages.
Practical use: STL algorithms
Sorting and comparison
std::vector<std::pair<int, std::string>> items = {{2, "b"}, {1, "a"}};
std::sort(items.begin(), items.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
You do not need to spell out the pair type for a sort on the first element.
Predicate search
auto it = std::find_if(vec.begin(), vec.end(),
[](const auto& e) { return e.id == target_id; });
e is deduced as the container’s value_type.
Transform and accumulate
std::transform(a.begin(), a.end(), out.begin(),
[](auto x) { return std::abs(x); });
int sum = std::accumulate(v.begin(), v.end(), 0,
[](auto acc, const auto& x) { return acc + x.size(); });
std::visit and variants
std::visit([](const auto& x) { std::cout << x; }, my_variant);
One lambda body can handle each alternative of std::variant (a distinct operator() instantiation per alternative).
Type deduction rules
- Parameter
auto: Same family asautoin a function template parameter—template arguments are deduced from the arguments at the call site. auto&/const auto&: Express intent to preserve lvalue-ness and constness.const auto&also binds to temporaries and is widely applicable.auto&&: Forwarding reference rules apply, so you can bind lvalues and rvalues efficiently (typical for “perfect forwarding” inside a generic lambda).- Return type: If omitted, all
returnpaths must deduce to a compatible type. Different types in branches (e.g.intvsdouble) cause a deduction conflict.
// Likely error: int vs double in different branches
// [](auto x) { return cond ? 0 : 1.0; } // Deduction clash
When needed, give the lambda an explicit return type:
[](auto x) -> double { return x * 1.0; }
Performance considerations
- Overhead: A lambda call is like a small inlineable function. Generics are still not virtual (standard lambdas are not polymorphic through vtables).
- Code size: Each distinct argument-type combination can produce another template instance. Hundreds of distinct signatures can bloat the binary; sometimes a non-template function or a single-type lambda is better.
- Capture:
[=],[&], etc.—captured state affects closure size whether or not the lambda is generic. - Optimization: Compilers often inline comparison lambdas for
std::sortaggressively. Do not assume “generic means slow”—measure before optimizing.
decltype and generic lambdas
For a parameter named x, decltype(x) reflects the actual argument type at the call. That is handy when you need a local “same type as x” in the body.
// Variable declaration and initialization
auto f = [](auto x) -> decltype(x) {
decltype(x) copy = x;
return copy;
};
Together with the decltype guide, this also clarifies differences from lambdas that use decltype(auto) as the return type.
constexpr generic lambdas (C++17)
If a lambda is constexpr and the body satisfies constexpr requirements, it can be evaluated at compile time. Generic lambdas behave the same: when arguments are literals (or otherwise constexpr-friendly), the corresponding template instance can be evaluated as a constexpr call.
constexpr auto sq = [](auto x) { return x * x; };
static_assert(sq(3) == 9);
Using non-literal-friendly types such as std::string as arguments usually forces runtime evaluation. See constexpr lambdas.
Common mistakes
- Only
autoby value but you meant to mutate elements: You modify a copy; container elements stay unchanged. Useauto&orauto*as appropriate. - Two different
autoparameters:auto a, auto bare two independent template parameters. To force the same type, a C++20 template lambda withtemplate<typename T> ... (T a, T b)is clearer. - Recursion: A nameless lambda is awkward to call from inside itself. Consider
std::function, a Y combinator pattern, or a named function object.
Summary
| Keyword | Meaning |
|---|---|
| Generic lambda | auto parameters → templated operator() |
| C++20 | Explicit template lambdas: []<typename T>(T x){} |
| STL | Less type repetition in sort, search, transform, visit |
| Performance | Inline, non-virtual; instance count scales with types used |
See also: Lambda capture, constexpr lambdas, decltype.