skip to content
Mehdi Mehdikhani
Table of Contents

Lambda functions are one of those C++11 features I didn’t appreciate until I started using them for more than just simple callbacks. They’re useful for decoupling code and making it more testable and flexible. Here’s what I’ve learned about their quirks and how I use them in practice.

The Basics and Particularities

Lambda functions in C++ have some interesting characteristics that aren’t immediately obvious:

auto simple_lambda = [](int x) { return x * 2; };
// Capture by value vs reference
int multiplier = 5;
auto by_value = [multiplier](int x) { return x * multiplier; };
auto by_ref = [&multiplier](int x) { return x * multiplier; };
// Mutable lambdas - can modify captured values
auto counter = [count = 0]() mutable { return ++count; };

The capture list is where lambdas get interesting. Capturing by value creates a copy, capturing by reference keeps a reference to the original. The mutable keyword lets you modify captured values even when they’re captured by value.

One thing that caught me off guard initially: lambdas are actually function objects (functors) under the hood. Each lambda creates a unique type, which is why auto is so useful with them.

Decoupling with Lambdas

Here’s a real example from a project where I needed to decouple a data processor from its notification logic. Originally, the code was tightly coupled:

// Tightly coupled - hard to test and extend
class DataProcessor {
EmailService email_service_;
LoggingService logger_;
public:
void processData(const std::vector<Data>& data) {
for (const auto& item : data) {
// Process the data
auto result = doSomeProcessing(item);
// Tightly coupled notifications
if (result.hasErrors()) {
email_service_.sendAlert("Processing failed for item " + item.id);
logger_.logError("Failed to process: " + item.id);
} else {
logger_.logInfo("Successfully processed: " + item.id);
}
}
}
};

After refactoring with lambdas, the code became much more flexible:

// Decoupled version using lambdas
class DataProcessor {
public:
using OnSuccess = std::function<void(const Data&, const ProcessResult&)>;
using OnError = std::function<void(const Data&, const ProcessResult&)>;
void processData(const std::vector<Data>& data,
OnSuccess on_success,
OnError on_error) {
for (const auto& item : data) {
auto result = doSomeProcessing(item);
if (result.hasErrors()) {
on_error(item, result);
} else {
on_success(item, result);
}
}
}
};
// Usage - inject behavior through lambdas
DataProcessor processor;
EmailService email_service;
LoggingService logger;
processor.processData(data,
// Success callback
[&logger](const Data& item, const ProcessResult& result) {
logger.logInfo("Successfully processed: " + item.id);
},
// Error callback
[&email_service, &logger](const Data& item, const ProcessResult& result) {
email_service.sendAlert("Processing failed for item " + item.id);
logger.logError("Failed to process: " + item.id + ", Error: " + result.getErrorMessage());
}
);

The Benefits I’ve Seen

This lambda-based approach gives me several advantages:

  1. Easy testing: I can inject simple lambdas for testing without mocking entire services:
// Testing becomes trivial
std::vector<std::string> success_messages;
std::vector<std::string> error_messages;
processor.processData(test_data,
[&](const Data& item, const ProcessResult&) {
success_messages.push_back(item.id);
},
[&](const Data& item, const ProcessResult&) {
error_messages.push_back(item.id);
}
);
// Assert on success_messages and error_messages
  1. Flexible behavior: Different parts of my application can use the same processor with completely different notification strategies.

  2. No interface bloat: I don’t need to create abstract base classes or interfaces just to inject different behaviors.

Lambda Capture Gotchas

One thing to watch out for is lambda lifetime and captures. This burned me once:

std::function<void()> createBadLambda() {
int local_var = 42;
return [&local_var]() { std::cout << local_var << std::endl; };
// local_var goes out of scope, but lambda still references it
}
std::function<void()> createGoodLambda() {
int local_var = 42;
return [local_var]() { std::cout << local_var << std::endl; }; // Safe - captures by value
}

When I Reach for Lambdas

I use lambdas for decoupling whenever I have:

  • Algorithms that need different behaviors injected
  • Event handling where different parts of the code need to respond differently
  • One-off customizations that don’t warrant a full class
  • Testing scenarios where I need to inject simple behaviors

They’ve become my go-to tool for keeping code flexible without the ceremony of traditional design patterns. Simple to use, and the code reads more clearly once you get used to the syntax.