Lambda Expressions In Depth
Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 85 of 104.
Lambda expressions, introduced in C++11, are anonymous functions you can define inline. While you've seen basic lambdas with STL algorithms, understanding their full syntax unlocks powerful capabilities for capturing variables and controlling how they're accessed.
The complete lambda syntax is: [capture](parameters) mutable -> return_type { body }. The capture clause determines which outside variables the lambda can access and how:
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // Copy of x
auto byRef = [&y]() { y += 5; }; // Reference to y
auto allByValue = [=]() { return x + y; }; // Copy all
auto allByRef = [&]() { x++; y++; }; // Reference all
auto mixed = [x, &y]() { y += x; }; // Mix both
byRef();
std::cout << y << "\n"; // 25
}By default, captured-by-value variables are const inside the lambda. The mutable keyword allows modification of these copies:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // Modifies the lambda's copy
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - original unchangedC++14 added init captures, letting you create new variables or move objects into the lambda:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Ownership transferred into lambdaLambdas are particularly useful in OOP when you need to pass behavior as a parameter - for callbacks, custom comparators, or event handlers - without defining separate function objects.
Challenge
EasyLet's build an event handler system that showcases the power of lambda expressions with different capture modes. You'll create a simple event dispatcher that stores and invokes callbacks, demonstrating how lambdas can capture external state in various ways.
You'll organize your code across three files:
EventDispatcher.h: Define anEventDispatcherclass that manages event callbacks.Your dispatcher should store callbacks using
std::vectorofstd::function<void()>. Include these methods:addCallback(std::function<void()> callback)— adds a callback to the listfireAll()— invokes all stored callbacks in orderclear()— removes all callbacks
You'll need to include
<functional>and<vector>.EventDispatcher.cpp: Implement the methods for your dispatcher. ThefireAll()method should simply iterate through all callbacks and invoke each one.main.cpp: Read two inputs:- A base number (integer)
- A multiplier (integer)
Create an
EventDispatcherand demonstrate different lambda capture techniques by adding three callbacks:- A lambda that captures the base number by value and prints:
Base value: [base] - A lambda that captures the multiplier by reference, increments it by 1, then prints:
Multiplier after increment: [multiplier] - A mutable lambda that captures a counter variable (initialized to 0) by value, increments it each time it's called, and prints:
Call count: [counter]
After adding all callbacks, call
fireAll()twice to see how the different capture modes behave across multiple invocations. Between the twofireAll()calls, print---as a separator.Finally, after both rounds, print the final value of the multiplier variable from main to show how the reference capture affected it:
Final multiplier: [multiplier]
For example, with inputs 10 and 5:
Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7With inputs 42 and 0:
Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2Notice the key behaviors: the by-value capture keeps the original base unchanged, the by-reference capture modifies the actual multiplier variable in main (accumulating across calls), and the mutable lambda's counter resets to 1 each round because each fireAll() invokes the same stored lambda object with its own internal copy that persists—but since we're storing std::function copies, observe how the mutable state behaves in your implementation.
Cheat sheet
Lambda expressions are anonymous functions that can be defined inline. The complete syntax is:
[capture](parameters) mutable -> return_type { body }Capture Clauses
The capture clause determines which outside variables the lambda can access and how:
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // Copy of x
auto byRef = [&y]() { y += 5; }; // Reference to y
auto allByValue = [=]() { return x + y; }; // Copy all variables
auto allByRef = [&]() { x++; y++; }; // Reference all variables
auto mixed = [x, &y]() { y += x; }; // Mix both modesMutable Lambdas
By default, captured-by-value variables are const inside the lambda. Use mutable to modify copies:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // Modifies the lambda's copy
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - original unchangedInit Captures (C++14)
Create new variables or move objects into the lambda:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Ownership transferred into lambdaUsing Lambdas with std::function
Store lambdas in std::function for callbacks and event handlers:
#include <functional>
#include <vector>
std::vector<std::function<void()>> callbacks;
callbacks.push_back([x]() { /* use x */ });
callbacks.push_back([&y]() { /* modify y */ });Try it yourself
#include <iostream>
#include "EventDispatcher.h"
using namespace std;
int main() {
int base;
int multiplier;
cin >> base;
cin >> multiplier;
EventDispatcher dispatcher;
// TODO: Add a lambda that captures base BY VALUE
// It should print: "Base value: [base]"
// TODO: Add a lambda that captures multiplier BY REFERENCE
// It should increment multiplier by 1, then print: "Multiplier after increment: [multiplier]"
// TODO: Add a MUTABLE lambda that captures a counter (initialized to 0) by value
// It should increment the counter and print: "Call count: [counter]"
// TODO: Call fireAll() to invoke all callbacks
// TODO: Print "---" as separator
// TODO: Call fireAll() again
// TODO: Print the final multiplier value: "Final multiplier: [multiplier]"
return 0;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container12Modern C++ Features
Move Semantics & RvaluesPerfect ForwardingLambda Expressions In Depthstd::function & std::bindconstexpr and constevalStructured Bindingsoptional, variant, any