Lambda Expressions — Capture, Parameters, Return Type, Generic Lambdas, IIFE
In this tutorial, you will learn about Lambda Expressions. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ lambda expressions create anonymous function objects at the point of use, capturing surrounding variables and supporting inline definition of callbacks, predicates, and short-lived functions.
What You'll Learn
You will write lambda expressions with various capture modes, use lambdas with STL algorithms and as callbacks, create generic lambdas (C++14) with auto parameters, understand capture lifetime and dangling references, and use IIFE (immediately invoked function expressions) for scoped initialization.
Why It Matters
Lambdas eliminate the boilerplate of defining separate functor classes for simple operations. Instead of writing a 5-line struct with operator(), you write a one-line lambda. They are essential for STL algorithms, parallel execution, asynchronous code, and callback-based APIs. Every modern C++ codebase uses lambdas extensively.
Learning Path
graph LR
A["49: Type Traits & Metaprogramming"] --> B["50: Lambda Expressions"]
B --> C["51: auto & decltype"]
C --> D["52: Move Semantics"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic Lambda Syntax
A lambda consists of capture [], parameters (), return type ->, and body {}.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// Simplest lambda: no capture, no parameters
auto greet = [] {
std::cout << "Hello from lambda!\n";
};
greet(); // Hello from lambda!
// Lambda with parameters
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(3, 4) << "\n"; // 7
// Return type can be deduced (usually)
auto mul = [](int a, int b) { return a * b; };
std::cout << mul(5, 6) << "\n"; // 30
// Using with STL algorithms
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
// Count evens with lambda
auto evenCount = std::count_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
std::cout << "Evens: " << evenCount << "\n"; // 4
// Sort descending
std::sort(v.begin(), v.end(),
[](int a, int b) { return a > b; });
for (int x : v) std::cout << x << " ";
std::cout << "\n"; // 8 7 6 5 4 3 2 1
}
Capture Modes
Lambdas can capture local variables by value or by reference.
#include <iostream>
int main() {
int x = 10;
int y = 20;
// Capture by value (copy)
auto byValue = [x]() {
// x = 5; // Error: x is const (captured by value)
return x + 1;
};
std::cout << byValue() << "\n"; // 11
std::cout << "x unchanged: " << x << "\n"; // 10
// Capture by reference
auto byRef = [&x]() {
x = 99; // Modifies original
return x;
};
std::cout << byRef() << "\n"; // 99
std::cout << "x modified: " << x << "\n"; // 99
// Mixed capture
auto mixed = [x, &y]() {
y = x + y;
return y;
};
// Default captures
auto defaultCopy = [=]() { // Capture all by value
return x + y;
};
auto defaultRef = [&]() { // Capture all by reference
x = 0;
y = 0;
};
// Init capture (C++14): move/capture expressions
auto init = [z = x + y]() { // z is initialized at lambda creation
return z;
};
std::cout << init() << "\n"; // 99 (x was modified above)
// Move unique_ptr into lambda
auto ptr = std::make_unique<int>(42);
auto moveCapture = [p = std::move(ptr)]() {
return *p;
};
std::cout << moveCapture() << "\n"; // 42
// ptr is now null (moved into lambda)
}
Mutable Lambdas
By default, operator() on a lambda is const. Use mutable to modify captured values.
#include <iostream>
int main() {
int count = 0;
// Without mutable: captures are const (read-only)
auto printer = [count]() mutable {
++count; // OK with mutable — modifies the lambda's copy
std::cout << "Called " << count << " times\n";
};
printer(); // Called 1 times
printer(); // Called 2 times
std::cout << "Original count: " << count << "\n"; // 0 (unchanged)
// Mutable with reference capture modifies original
auto refCount = [&count]() mutable {
++count; // Modifies original through reference
};
refCount();
std::cout << "Original after ref: " << count << "\n"; // 1
}
Generic Lambdas (C++14)
Parameters can be auto, making lambdas generic — equivalent to function templates.
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
int main() {
// Generic lambda: T is deduced per call
auto print = [](const auto& value) {
std::cout << value << "\n";
};
print(42); // 42
print(3.14); // 3.14
print("hello"); // hello
print(std::string("world")); // world
// Generic lambda with two parameters
auto add = [](const auto& a, const auto& b) {
return a + b;
};
std::cout << add(3, 4) << "\n"; // 7
std::cout << add(1.5, 2.5) << "\n"; // 4.0
std::cout << add(std::string("a"), "b") << "\n"; // ab
// Use with any container
auto sortAndPrint = [](auto& container) {
std::sort(container.begin(), container.end());
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << "\n";
};
std::vector<int> vi = {3, 1, 4, 1, 5};
sortAndPrint(vi); // 1 1 3 4 5
std::vector<std::string> vs = {"banana", "apple", "cherry"};
sortAndPrint(vs); // apple banana cherry
// Templated lambda (C++20): explicit template parameters
auto templated = []<typename T>(const std::vector<T>& vec) {
return vec.size();
};
std::cout << templated(vi) << "\n"; // 5
}
IIFE — Immediately Invoked Function Expression
Lambdas can be called immediately after definition, useful for scoped initialization.
#include <iostream>
#include <vector>
#include <algorithm>
class ExpensiveResource {
public:
ExpensiveResource() { std::cout << "Created\n"; }
~ExpensiveResource() { std::cout << "Destroyed\n"; }
void use() { std::cout << "Using\n"; }
};
int main() {
// IIFE: create + call immediately
// Useful for initializing const values that need computation
const auto data = []() {
std::vector<int> temp(1000);
std::generate(temp.begin(), temp.end(), [n = 0]() mutable { return n++; });
// Complex initialization logic here
return temp;
}(); // Called immediately
std::cout << "Data size: " << data.size() << "\n"; // 1000
// IIFE for scoped resource management
{
auto resource = []() {
auto res = std::make_unique<ExpensiveResource>();
res->use();
return res;
}(); // Created, used
std::cout << "Inside scope\n";
} // Destroyed when unique_ptr goes out of scope
std::cout << "Outside scope\n";
}
Capturing this and Member Variables
#include <iostream>
class Counter {
int count_ = 0;
public:
void increment() { ++count_; }
auto getLambda() {
// Capture this by value (copies the pointer)
return [this]() { return count_; };
// Capture *this by copy (C++17) — captures entire object
// return [*this]() { return count_; };
}
auto getMutableLambda() {
return [this]() mutable {
return ++count_;
};
}
};
int main() {
Counter c;
c.increment();
c.increment();
auto lambda = c.getLambda();
std::cout << lambda() << "\n"; // 2
auto mutableLambda = c.getMutableLambda();
std::cout << mutableLambda() << "\n"; // 3
std::cout << mutableLambda() << "\n"; // 4
// *this capture (C++17): captures the object by value
// Any modifications inside the lambda do NOT affect the original
}
Lambda to Function Pointer
Non-capturing lambdas can convert to function pointers.
#include <iostream>
void execute(void(*func)()) {
func();
}
int main() {
// Non-capturing lambda: converts to function pointer
auto lambda = []() {
std::cout << "Called via function pointer\n";
};
execute(lambda); // OK: converts automatically
// execute([]() { /* ... */ }); // Same
// Capturing lambda: cannot convert to function pointer
int x = 5;
auto capturing = [x]() {
std::cout << x << "\n";
};
// execute(capturing); // Error: cannot convert
}
Common Mistakes
Mistake 1: Dangling references in capture
auto badLambda() {
int x = 5;
return [&x]() { return x; }; // x is destroyed when function returns!
}
Capture by value [x] or use std::shared_ptr for extending lifetimes.
Mistake 2: Modifying captured values without mutable
int count = 0;
auto lambda = [count]() { ++count; }; // Error: count is const
Add mutable keyword: [count]() mutable { ++count; }.
Mistake 3: Capturing all by reference in long-lived lambdas
auto lambda = [&]() { /* ... */ }; // Captures everything by reference
Default reference capture can cause dangling references. Be explicit.
Mistake 4: Overlooking that each lambda has a unique type
auto a = []{};
auto b = []{};
// a and b have different types!
// cannot assign a = b
Mistake 5: Recursive lambda is tricky
auto fib = [](int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2); // Error: fib not yet defined
};
Use std::function or Y-combinator pattern for recursive lambdas.
Practice Questions
- What is the output?
int x = 5;
auto l = [&x]() { return ++x; };
std::cout << l() << " " << x;
Answer: 6 6 — reference capture modifies the original.
What does
mutabledo in a lambda? Answer: Allows the lambda to modify its value-captured members. Withoutmutable, captured values areconst.Write a lambda that computes the sum of a vector.
auto sum = [](const std::vector<int>& v) {
int total = 0;
for (int x : v) total += x;
return total;
};
Can a capturing lambda be converted to a function pointer? Answer: No. Only non-capturing lambdas (empty
[]) can convert to function pointers.What is an IIFE and why use it? Answer: Immediately Invoked Function Expression — a lambda defined and called in one step. Used for initializing const values with complex logic.
FAQ
Mini Project
Build a simple pipeline system using lambdas — a series of transformations applied to data:
#include <iostream>
#include <vector>
#include <functional>
// Pipeline class that composes lambdas
class Pipeline {
// Your implementation here
};
int main() {
Pipeline p;
p.then([](int x) { return x * 2; })
.then([](int x) { return x + 1; })
.then([](int x) { return x * x; });
std::cout << p.execute(5) << "\n"; // ((5*2)+1)^2 = 121
std::cout << p.execute(0) << "\n"; // ((0*2)+1)^2 = 1
std::cout << p.execute(-3) << "\n"; // ((-3*2)+1)^2 = 25
}
This project demonstrates how C++ lambdas enable functional-style composition, similar to how Java uses method references and lambda chains in streams.
What's Next
You now write concise lambdas — the most used C++11 feature. Next, you will learn auto and decltype, the type deduction mechanisms that make generic code cleaner and more maintainable.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro