Menu
Coddy logo textTech

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 unchanged

C++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 lambda

Lambdas 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 icon

Challenge

Easy

Let'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 an EventDispatcher class that manages event callbacks.

    Your dispatcher should store callbacks using std::vector of std::function<void()>. Include these methods:

    • addCallback(std::function<void()> callback) — adds a callback to the list
    • fireAll() — invokes all stored callbacks in order
    • clear() — removes all callbacks

    You'll need to include <functional> and <vector>.

  • EventDispatcher.cpp: Implement the methods for your dispatcher. The fireAll() method should simply iterate through all callbacks and invoke each one.
  • main.cpp: Read two inputs:
    1. A base number (integer)
    2. A multiplier (integer)

    Create an EventDispatcher and demonstrate different lambda capture techniques by adding three callbacks:

    1. A lambda that captures the base number by value and prints: Base value: [base]
    2. A lambda that captures the multiplier by reference, increments it by 1, then prints: Multiplier after increment: [multiplier]
    3. 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 two fireAll() 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: 7

With 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: 2

Notice 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 modes

Mutable 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 unchanged

Init 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 lambda

Using 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;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming