C++ Lambda Expressions: Complete Guide and Common Scenarios
C++ Lambda Expressions: Complete Guide and Common Scenarios
Lambda expressions in C++ provide a concise way to define anonymous function objects. This guide covers lambda syntax, capture modes, and common real-world scenarios.
Lambda Basics
Basic Syntax
// Basic syntax: [capture](parameters) -> return_type { body }
// Simple lambda
auto add = [](int a, int b) { return a + b; };
int result = add(3, 4); // 7
// Lambda with explicit return type
auto multiply = [](int a, int b) -> int { return a * b; };
// Lambda without parameters
auto get_answer = []() { return 42; };
int answer = get_answer();
// Lambda with single parameter (parentheses optional)
auto square = [](int x) { return x * x; };
auto square2 = [](int x) { return x * x; }; // Same as above
Lambda as Function Parameter
#include <vector>
#include <algorithm>
std::vector<int> vec = {1, 2, 3, 4, 5};
// Lambda with STL algorithms
std::for_each(vec.begin(), vec.end(), [](int& n) {
n *= 2;
});
// Find with lambda
auto it = std::find_if(vec.begin(), vec.end(), [](int n) {
return n > 5;
});
// Count with lambda
int count = std::count_if(vec.begin(), vec.end(), [](int n) {
return n % 2 == 0;
});
// Transform with lambda
std::transform(vec.begin(), vec.end(), vec.begin(), [](int n) {
return n * n;
});
Capture Modes
1. Capture by Value [=]
int x = 10;
int y = 20;
// Capture all by value
auto lambda = [=](int a) {
return a + x + y; // x and y are copied
};
int result = lambda(5); // 35
// x and y are not modified
std::cout << x << " " << y << std::endl; // 10 20
2. Capture by Reference [&]
int x = 10;
int y = 20;
// Capture all by reference
auto lambda = [&](int a) {
x = 100; // Modifies original x
y = 200; // Modifies original y
return a + x + y;
};
int result = lambda(5); // 305
std::cout << x << " " << y << std::endl; // 100 200
3. Mixed Capture [x, &y]
int x = 10;
int y = 20;
int z = 30;
// Capture specific variables
auto lambda = [x, &y, z](int a) {
// x and z are copied, y is referenced
y = 200; // Modifies original y
return a + x + y + z;
};
int result = lambda(5); // 245
std::cout << x << " " << y << " " << z << std::endl; // 10 200 30
4. Capture with Initialization [var = expr]
// C++14: Capture with initializer
int x = 10;
// Move capture
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
};
// Copy with modification
auto lambda2 = [y = x + 5]() {
return y; // y is 15, independent of x
};
// Reference capture with initializer
auto lambda3 = [&ref = x]() {
ref = 100; // Modifies x
};
5. mutable Keyword
// Without mutable: captured by-value variables are const
int counter = 0;
auto lambda = [counter]() mutable {
counter++; // Can modify copy, not original
return counter;
};
lambda(); // Returns 1
lambda(); // Returns 2
std::cout << counter << std::endl; // Still 0 (original unchanged)
Common Scenarios
Scenario 1: Callbacks
Event Handlers
#include <functional>
#include <vector>
#include <string>
class Button {
public:
using ClickHandler = std::function<void()>;
void setOnClick(ClickHandler handler) {
onClick_ = handler;
}
void click() {
if (onClick_) {
onClick_();
}
}
private:
ClickHandler onClick_;
};
// Usage
Button button;
button.setOnClick([]() {
std::cout << "Button clicked!" << std::endl;
});
button.click(); // Prints "Button clicked!"
// With captured state
int clickCount = 0;
button.setOnClick([&clickCount]() {
clickCount++;
std::cout << "Clicked " << clickCount << " times" << std::endl;
});
button.click(); // Clicked 1 times
button.click(); // Clicked 2 times
Progress Callbacks
#include <functional>
#include <thread>
#include <chrono>
class TaskProcessor {
public:
using ProgressCallback = std::function<void(int)>;
void setProgressCallback(ProgressCallback callback) {
progressCallback_ = callback;
}
void process() {
for (int i = 0; i <= 100; i += 10) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (progressCallback_) {
progressCallback_(i);
}
}
}
private:
ProgressCallback progressCallback_;
};
// Usage
TaskProcessor processor;
processor.setProgressCallback([](int progress) {
std::cout << "Progress: " << progress << "%" << std::endl;
});
processor.process();
Completion Callbacks
#include <functional>
#include <future>
#include <thread>
template<typename T>
class AsyncTask {
public:
using CompletionCallback = std::function<void(const T&)>;
using ErrorCallback = std::function<void(const std::string&)>;
void execute(T data, CompletionCallback onSuccess, ErrorCallback onError) {
std::thread([=]() {
try {
// Simulate async work
std::this_thread::sleep_for(std::chrono::seconds(1));
T result = processData(data);
onSuccess(result);
} catch (const std::exception& e) {
onError(e.what());
}
}).detach();
}
private:
T processData(const T& data) {
return data; // Simplified
}
};
// Usage
AsyncTask<int> task;
task.execute(42,
[](const int& result) {
std::cout << "Success: " << result << std::endl;
},
[](const std::string& error) {
std::cerr << "Error: " << error << std::endl;
}
);
Scenario 2: STL Algorithms
Custom Comparators
#include <vector>
#include <algorithm>
#include <string>
// Sort with custom comparator
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // Descending order
});
// Sort objects
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// Sort by age
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
return a.age < b.age;
});
// Sort by name
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
return a.name < b.name;
});
Filtering and Transformation
#include <vector>
#include <algorithm>
#include <iterator>
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter: keep only evens
std::vector<int> evens;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evens),
[](int n) { return n % 2 == 0; });
// Transform: square all numbers
std::vector<int> squares;
std::transform(numbers.begin(), numbers.end(), std::back_inserter(squares),
[](int n) { return n * n; });
// Remove if
numbers.erase(
std::remove_if(numbers.begin(), numbers.end(),
[](int n) { return n > 5; }),
numbers.end()
);
Accumulation
#include <vector>
#include <numeric>
#include <string>
std::vector<int> vec = {1, 2, 3, 4, 5};
// Sum with lambda
int sum = std::accumulate(vec.begin(), vec.end(), 0,
[](int acc, int val) { return acc + val; });
// Product
int product = std::accumulate(vec.begin(), vec.end(), 1,
[](int acc, int val) { return acc * val; });
// String concatenation
std::vector<std::string> words = {"Hello", " ", "World", "!"};
std::string result = std::accumulate(words.begin(), words.end(), std::string(),
[](const std::string& acc, const std::string& val) {
return acc + val;
});
Scenario 3: Event-Driven Programming
#include <functional>
#include <vector>
#include <string>
#include <map>
class EventEmitter {
public:
using EventHandler = std::function<void(const std::string&)>;
void on(const std::string& event, EventHandler handler) {
handlers_[event].push_back(handler);
}
void emit(const std::string& event, const std::string& data) {
if (handlers_.find(event) != handlers_.end()) {
for (auto& handler : handlers_[event]) {
handler(data);
}
}
}
private:
std::map<std::string, std::vector<EventHandler>> handlers_;
};
// Usage
EventEmitter emitter;
// Register handlers
emitter.on("data", [](const std::string& data) {
std::cout << "Received data: " << data << std::endl;
});
emitter.on("error", [](const std::string& error) {
std::cerr << "Error: " << error << std::endl;
});
// Emit events
emitter.emit("data", "Hello, World!");
emitter.emit("error", "Something went wrong");
Scenario 4: Functional Programming Patterns
Higher-Order Functions
#include <functional>
#include <vector>
// Function that returns a lambda
auto make_multiplier(int factor) {
return [factor](int x) { return x * factor; };
}
auto double_it = make_multiplier(2);
auto triple_it = make_multiplier(3);
std::cout << double_it(5) << std::endl; // 10
std::cout << triple_it(5) << std::endl; // 15
// Function composition
auto compose = [](auto f, auto g) {
return [f, g](auto x) { return f(g(x)); };
};
auto add_one = [](int x) { return x + 1; };
auto square = [](int x) { return x * x; };
auto add_one_then_square = compose(square, add_one);
std::cout << add_one_then_square(5) << std::endl; // 36
Currying
// Curried function
auto curry_add = [](int a) {
return [a](int b) {
return [a, b](int c) {
return a + b + c;
};
};
};
auto add_10 = curry_add(10);
auto add_10_20 = add_10(20);
int result = add_10_20(30); // 60
// Or call directly
int result2 = curry_add(10)(20)(30); // 60
Scenario 5: Async Programming
Thread Pool Tasks
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
public:
using Task = std::function<void()>;
ThreadPool(size_t num_threads) {
for (size_t i = 0; i < num_threads; ++i) {
workers_.emplace_back([this]() {
while (true) {
Task task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
condition_.wait(lock, [this] { return stop_ || !tasks_.empty(); });
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
template<typename F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
tasks_.emplace(std::forward<F>(f));
}
condition_.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex_);
stop_ = true;
}
condition_.notify_all();
for (auto& worker : workers_) {
worker.join();
}
}
private:
std::vector<std::thread> workers_;
std::queue<Task> tasks_;
std::mutex queue_mutex_;
std::condition_variable condition_;
bool stop_ = false;
};
// Usage
ThreadPool pool(4);
for (int i = 0; i < 10; ++i) {
pool.enqueue([i]() {
std::cout << "Task " << i << " executed" << std::endl;
});
}
Promise/Future with Lambdas
#include <future>
#include <thread>
// Async task with lambda
auto future = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
});
int result = future.get(); // 42
// Multiple async tasks
std::vector<std::future<int>> futures;
for (int i = 0; i < 5; ++i) {
futures.push_back(std::async(std::launch::async, [i]() {
return i * i;
}));
}
for (auto& f : futures) {
std::cout << f.get() << std::endl;
}
Scenario 6: Configuration and Settings
#include <functional>
#include <map>
#include <string>
class ConfigManager {
public:
using Validator = std::function<bool(const std::string&)>;
using Transformer = std::function<std::string(const std::string&)>;
void registerValidator(const std::string& key, Validator validator) {
validators_[key] = validator;
}
void registerTransformer(const std::string& key, Transformer transformer) {
transformers_[key] = transformer;
}
bool validate(const std::string& key, const std::string& value) {
if (validators_.find(key) != validators_.end()) {
return validators_[key](value);
}
return true;
}
std::string transform(const std::string& key, const std::string& value) {
if (transformers_.find(key) != transformers_.end()) {
return transformers_[key](value);
}
return value;
}
private:
std::map<std::string, Validator> validators_;
std::map<std::string, Transformer> transformers_;
};
// Usage
ConfigManager config;
// Register email validator
config.registerValidator("email", [](const std::string& email) {
return email.find('@') != std::string::npos;
});
// Register uppercase transformer
config.registerTransformer("name", [](const std::string& name) {
std::string upper = name;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
return upper;
});
bool valid = config.validate("email", "user@example.com");
std::string transformed = config.transform("name", "john");
Scenario 7: State Machines
#include <functional>
#include <map>
class StateMachine {
public:
using StateHandler = std::function<void()>;
using TransitionCondition = std::function<bool()>;
void addState(const std::string& state, StateHandler handler) {
states_[state] = handler;
}
void addTransition(const std::string& from, const std::string& to,
TransitionCondition condition) {
transitions_[from][to] = condition;
}
void setState(const std::string& state) {
current_state_ = state;
}
void update() {
if (states_.find(current_state_) != states_.end()) {
states_[current_state_]();
}
// Check transitions
if (transitions_.find(current_state_) != transitions_.end()) {
for (auto& [next_state, condition] : transitions_[current_state_]) {
if (condition()) {
current_state_ = next_state;
break;
}
}
}
}
private:
std::string current_state_;
std::map<std::string, StateHandler> states_;
std::map<std::string, std::map<std::string, TransitionCondition>> transitions_;
};
// Usage
StateMachine sm;
int counter = 0;
sm.addState("idle", []() {
std::cout << "Idle state" << std::endl;
});
sm.addState("active", [&counter]() {
counter++;
std::cout << "Active: " << counter << std::endl;
});
sm.addTransition("idle", "active", [&counter]() {
return counter < 5;
});
sm.setState("idle");
sm.update();
Scenario 8: Decorator Pattern
#include <functional>
#include <string>
class Logger {
public:
using LogFunction = std::function<void(const std::string&)>;
void setLogFunction(LogFunction func) {
log_func_ = func;
}
void log(const std::string& message) {
if (log_func_) {
log_func_(message);
}
}
private:
LogFunction log_func_;
};
// Usage with decorators
Logger logger;
// Basic logging
logger.setLogFunction([](const std::string& msg) {
std::cout << msg << std::endl;
});
// With timestamp decorator
logger.setLogFunction([](const std::string& msg) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::cout << "[" << std::ctime(&time) << "] " << msg << std::endl;
});
// With level decorator
std::string level = "INFO";
logger.setLogFunction([level](const std::string& msg) {
std::cout << "[" << level << "] " << msg << std::endl;
});
Scenario 9: Retry Logic
#include <functional>
#include <thread>
#include <chrono>
template<typename T>
T retry(std::function<T()> func, int max_attempts, int delay_ms) {
for (int i = 0; i < max_attempts; ++i) {
try {
return func();
} catch (const std::exception& e) {
if (i == max_attempts - 1) throw;
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
}
}
throw std::runtime_error("Max attempts reached");
}
// Usage
int result = retry<int>([]() {
// Simulate operation that might fail
static int attempts = 0;
if (++attempts < 3) {
throw std::runtime_error("Failed");
}
return 42;
}, 5, 100);
Scenario 10: Observer Pattern
#include <functional>
#include <vector>
#include <string>
template<typename T>
class Observable {
public:
using Observer = std::function<void(const T&)>;
void subscribe(Observer observer) {
observers_.push_back(observer);
}
void notify(const T& data) {
for (auto& observer : observers_) {
observer(data);
}
}
private:
std::vector<Observer> observers_;
};
// Usage
Observable<int> observable;
// Subscribe observers
observable.subscribe([](const int& value) {
std::cout << "Observer 1: " << value << std::endl;
});
observable.subscribe([](const int& value) {
std::cout << "Observer 2: " << value * 2 << std::endl;
});
// Notify all observers
observable.notify(42);
Advanced Topics
Generic Lambdas (C++14)
// Generic lambda with auto
auto generic_add = [](auto a, auto b) {
return a + b;
};
int result1 = generic_add(3, 4); // 7
double result2 = generic_add(3.5, 2.1); // 5.6
std::string result3 = generic_add(std::string("hello"), std::string(" world"));
Lambda with Template Parameters (C++20)
// C++20: Lambda with template parameters
auto lambda = []<typename T>(T value) {
return value * 2;
};
int result = lambda(5); // 10
double result2 = lambda(3.14); // 6.28
Recursive Lambdas
// Recursive lambda with std::function
std::function<int(int)> factorial = [&factorial](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
int result = factorial(5); // 120
// Or with Y-combinator pattern
auto y_combinator = [](auto f) {
return [f](auto... args) {
return f(f, args...);
};
};
auto factorial2 = y_combinator([](auto self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
});
Lambda as Return Type
auto make_counter() {
int count = 0;
return [count]() mutable {
return ++count;
};
}
auto counter = make_counter();
std::cout << counter() << std::endl; // 1
std::cout << counter() << std::endl; // 2
Best Practices
- Prefer lambdas for short, local functions
- Use meaningful capture lists - be explicit about what you capture
- Avoid capturing large objects by value - use references or move
- Use
mutableonly when necessary - Consider
std::functionfor type erasure when needed - Use generic lambdas (C++14+) for template-like behavior
- Be careful with lifetime - ensure captured references remain valid
Common Pitfalls
1. Dangling References
// BAD: Dangling reference
std::function<int()> get_lambda() {
int x = 10;
return [&x]() { return x; }; // x is destroyed when function returns
}
// GOOD: Capture by value
std::function<int()> get_lambda_safe() {
int x = 10;
return [x]() { return x; }; // x is copied
}
2. Unexpected Mutations
int x = 10;
auto lambda = [&x]() {
x = 100; // Modifies original x
};
lambda();
std::cout << x << std::endl; // 100 (unexpected if you thought it was copied)
3. Performance Considerations
// std::function has overhead
std::function<int(int)> func = [](int x) { return x * 2; };
// Direct lambda is faster
auto lambda = [](int x) { return x * 2; };
// Use auto for lambda types when possible
Summary
Lambdas in C++ are powerful tools for:
- Callbacks and event handlers
- STL algorithm customization
- Functional programming patterns
- Async programming
- Configuration and validation
- State machines and observers
Understanding capture modes, common patterns, and best practices will help you write more expressive and maintainable C++ code.