Chapter 20 of 23

Lambda Functions and Functional Programming

Master C++ lambda expressions, closures, std::function, std::bind, and functional-style programming with STL algorithms to write cleaner, more expressive code.

Meritshot11 min read
C++LambdaFunctional ProgrammingSTLstd::functionstd::bind
All C++ Chapters

Lambda Functions and Functional Programming

Before C++11, writing a custom comparator or a small callback meant defining a named function or a functor class — often far away from the code that actually used it. Lambda expressions changed everything. They let you write anonymous functions inline, right where you need them. Today, lambdas are everywhere in modern C++ — from STL algorithms to multithreading callbacks to event-driven systems at companies like Flipkart, Zomato, and Google India.

This chapter gives you a thorough understanding of lambda syntax, capture semantics, generic lambdas, std::function, std::bind, and how to combine all of these into a real data-filtering pipeline.


What Is a Lambda Expression?

A lambda is a concise way to define an anonymous function object directly in your source code. The compiler generates a unique class (a closure type) for each lambda you write, with an overloaded operator(). You never see this class — you just use the lambda.

The full syntax is:

[capture-list](parameter-list) -> return_type {
    // body
};

A minimal lambda that adds two integers:

#include <iostream>

int main() {
    auto add = [](int a, int b) -> int {
        return a + b;
    };

    std::cout << add(3, 7) << "\n"; // 10
}

The return type (-> int) can almost always be omitted — the compiler deduces it from the return statement. The version below is equivalent:

auto add = [](int a, int b) { return a + b; };

The Capture List — Accessing the Surrounding Scope

The capture list (inside [ ]) controls which local variables from the enclosing scope the lambda can see.

Capture by Value ([=] or [x])

The lambda gets its own copy of the variable at the moment the lambda is created.

#include <iostream>

int main() {
    int discount = 10;

    auto applyDiscount = [discount](int price) {
        return price - discount; // uses a copy of discount
    };

    discount = 50; // changing the original has no effect
    std::cout << applyDiscount(200) << "\n"; // 190, not 150
}

Capture by Reference ([&] or [&x])

The lambda holds a reference to the original variable. Any change inside the lambda affects the original — and vice versa.

#include <iostream>

int main() {
    int count = 0;

    auto increment = [&count]() {
        count++;
    };

    increment();
    increment();
    std::cout << count << "\n"; // 2
}

Warning: Capturing by reference is dangerous if the lambda outlives the local variable. Never store a lambda that captures local variables by reference if that lambda may be called after the function returns.

Mixed Captures

You can mix captures explicitly:

int base = 100;
int bonus = 20;

// capture base by value, bonus by reference
auto salary = [base, &bonus](int years) {
    return base * years + bonus;
};

Default Captures

SyntaxMeaning
[=]Capture all locals by value
[&]Capture all locals by reference
[=, &x]Capture all by value, but x by reference
[&, y]Capture all by reference, but y by value

Prefer explicit captures (listing only what you need) over [=] or [&] — it makes code easier to read and reason about.


Mutable Lambdas

By default, a lambda captured by value cannot modify its captured copies (they are const). The mutable keyword removes this restriction:

#include <iostream>

int main() {
    int callCount = 0;

    // Without mutable, this would fail to compile
    auto counter = [callCount]() mutable {
        callCount++;
        std::cout << "Called " << callCount << " times\n";
    };

    counter(); // Called 1 times
    counter(); // Called 2 times

    // The original callCount is still 0
    std::cout << "Original: " << callCount << "\n"; // 0
}

mutable lambdas are useful when the lambda needs internal state but you do not want it to modify the outer variable.


Generic Lambdas (C++14)

Before C++14, every lambda parameter needed an explicit type. C++14 introduced auto parameters, making lambdas polymorphic — similar to a function template:

#include <iostream>
#include <string>

int main() {
    auto printTwice = [](auto val) {
        std::cout << val << " " << val << "\n";
    };

    printTwice(42);           // int
    printTwice(3.14);         // double
    printTwice(std::string("Meritshot")); // std::string
}

Under the hood, the compiler generates a templated operator() for the closure class. This lets a single lambda work with multiple types without any code duplication.


std::function — Type-Erased Callable Wrapper

Lambdas have unique, unnameable closure types. When you need to store a callable in a variable with a known type (e.g., in a vector or a class member), use std::function:

#include <iostream>
#include <functional>

int main() {
    std::function<int(int, int)> op;

    op = [](int a, int b) { return a + b; };
    std::cout << op(3, 4) << "\n"; // 7

    op = [](int a, int b) { return a * b; };
    std::cout << op(3, 4) << "\n"; // 12
}

std::function<R(Args...)> can hold any callable — a lambda, a regular function pointer, or a functor — that matches the signature R(Args...).

Performance note: std::function involves type erasure (virtual dispatch internally) and can add overhead. For hot paths, prefer templates or auto to avoid the cost.


std::bind — Partial Application

std::bind (from <functional>) creates a new callable by fixing some arguments of an existing function:

#include <iostream>
#include <functional>

int multiply(int a, int b) {
    return a * b;
}

int main() {
    // Fix the first argument to 5
    auto triple = std::bind(multiply, 3, std::placeholders::_1);

    std::cout << triple(10) << "\n"; // 30
    std::cout << triple(7)  << "\n"; // 21
}

std::placeholders::_1, _2, etc. mark the positions of arguments that the caller will supply. In modern C++, a lambda is almost always clearer than std::bind:

// Prefer this over std::bind
auto triple = [](int x) { return multiply(3, x); };

Use std::bind when working with legacy APIs that expect it, or when you need to reorder arguments in complex ways.


Passing Lambdas to STL Algorithms

This is where lambdas truly shine. Every STL algorithm that accepts a predicate or comparator becomes much more expressive with lambdas.

Sorting with a Custom Comparator

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

int main() {
    // Sort students by score descending, then by name ascending
    struct Student { std::string name; int score; };

    std::vector<Student> students = {
        {"Arjun", 88}, {"Priya", 95}, {"Rahul", 88}, {"Sneha", 72}
    };

    std::sort(students.begin(), students.end(),
        [](const Student& a, const Student& b) {
            if (a.score != b.score) return a.score > b.score;
            return a.name < b.name;
        }
    );

    for (const auto& s : students) {
        std::cout << s.name << ": " << s.score << "\n";
    }
    // Priya: 95, Arjun: 88, Rahul: 88, Sneha: 72
}

Filtering with std::copy_if

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> salaries = {35000, 80000, 120000, 45000, 200000, 60000};
    std::vector<int> highPay;

    int threshold = 70000;

    std::copy_if(salaries.begin(), salaries.end(),
                 std::back_inserter(highPay),
                 [threshold](int s) { return s >= threshold; });

    for (int s : highPay) std::cout << s << " "; // 80000 120000 200000
    std::cout << "\n";
}

Transforming with std::transform

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> prices = {500, 1200, 800, 2000};
    std::vector<int> discounted(prices.size());

    int pct = 10;
    std::transform(prices.begin(), prices.end(), discounted.begin(),
                   [pct](int p) { return p - p * pct / 100; });

    for (int d : discounted) std::cout << d << " "; // 450 1080 720 1800
    std::cout << "\n";
}

Immediately-Invoked Lambdas

You can define and call a lambda in the same expression, like an IIFE (immediately-invoked function expression) in JavaScript. This is useful for initialising a const variable with complex logic:

#include <iostream>
#include <vector>

int main() {
    const int maxScore = []() {
        std::vector<int> scores = {78, 92, 55, 88, 99, 61};
        int best = scores[0];
        for (int s : scores) if (s > best) best = s;
        return best;
    }(); // <-- immediately invoked

    std::cout << "Top score: " << maxScore << "\n"; // 99
}

This keeps maxScore const while allowing the initialisation logic to be as complex as needed.


Worked Example: A Configurable Data-Filtering Pipeline

In real-world backend systems — such as those at Swiggy, CRED, or Infosys — you often need to build flexible data pipelines: filter records, transform them, and sort them, all driven by runtime configuration. Lambdas make this elegant.

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include <string>

struct Employee {
    std::string name;
    std::string department;
    int salary;         // in INR
    int yearsExp;
};

// A pipeline accepts a vector and a list of predicates
std::vector<Employee> applyFilters(
    const std::vector<Employee>& data,
    const std::vector<std::function<bool(const Employee&)>>& filters)
{
    std::vector<Employee> result;
    std::copy_if(data.begin(), data.end(), std::back_inserter(result),
        [&filters](const Employee& e) {
            for (const auto& f : filters) {
                if (!f(e)) return false;
            }
            return true;
        });
    return result;
}

int main() {
    std::vector<Employee> employees = {
        {"Ananya", "Engineering", 120000, 5},
        {"Rohan",  "Sales",       60000,  2},
        {"Divya",  "Engineering", 95000,  3},
        {"Karan",  "HR",          55000,  7},
        {"Priya",  "Engineering", 140000, 8},
        {"Saurav", "Sales",       80000,  4},
    };

    // Build filters dynamically
    std::string targetDept = "Engineering";
    int minSalary = 100000;
    int minExp = 4;

    std::vector<std::function<bool(const Employee&)>> filters = {
        [&targetDept](const Employee& e) { return e.department == targetDept; },
        [minSalary](const Employee& e)   { return e.salary >= minSalary; },
        [minExp](const Employee& e)      { return e.yearsExp >= minExp; },
    };

    auto result = applyFilters(employees, filters);

    // Sort results by salary descending
    std::sort(result.begin(), result.end(),
        [](const Employee& a, const Employee& b) {
            return a.salary > b.salary;
        });

    std::cout << "Filtered & sorted results:\n";
    for (const auto& e : result) {
        std::cout << e.name << " | " << e.department
                  << " | INR " << e.salary
                  << " | " << e.yearsExp << " yrs\n";
    }
    // Priya | Engineering | INR 140000 | 8 yrs
    // Ananya | Engineering | INR 120000 | 5 yrs
}

This pattern is extremely common in product companies: each filter is a lambda that captures its configuration from the enclosing scope, the pipeline itself is generic, and sorting is a one-liner.


Common Pitfalls

1. Dangling reference captures If you capture a local variable by reference and the lambda outlives that variable (e.g., stored in a member, returned from a function), you get undefined behaviour. Always ensure the captured variable's lifetime exceeds the lambda's.

2. Capturing this incorrectly Inside a member function, [=] captures this by value (the pointer, not the object). Modifying members through a captured-by-value [=] still modifies the real object. Use [*this] (C++17) to capture a copy of the entire object.

3. std::function overhead on hot paths std::function has type-erasure overhead (heap allocation, virtual dispatch). Avoid it in tight loops; prefer auto or templates instead.

4. Forgetting mutable when you need it If you try to modify a by-value captured variable without mutable, the compiler will refuse. Remember: the closure's operator() is const by default.

5. Unintended copies with [=] [=] copies every local variable in scope — including large containers. Always prefer explicit named captures: [vec, threshold] instead of [=].


Practice Exercises

  1. Write a lambda that captures a std::vector<int> by reference and removes all elements smaller than a given threshold (passed as a parameter). Test with the vector {5, 12, 3, 8, 20, 1} and threshold 7.

  2. Create a std::vector<std::function<int(int)>> holding three lambdas: one that doubles its input, one that adds 10, and one that squares it. Iterate over the vector and apply each function to the number 5.

  3. Using std::sort and a lambda, sort a vector of strings by their length (ascending), breaking ties alphabetically.

  4. Implement a generic compose function that takes two std::function<int(int)> objects and returns a new std::function<int(int)> that applies the second, then the first (i.e., f(g(x))).

  5. Extend the employee filtering pipeline example to add a fourth filter: employees whose names start with a vowel. Add the filter as a lambda and verify the output.


Summary

  • A lambda has the form [capture](params) -> return_type { body }. Return type is usually deduced automatically.
  • Capture by value ([x] or [=]) gives the lambda its own copy; capture by reference ([&x] or [&]) gives a reference to the original.
  • mutable allows a by-value capture to be modified inside the lambda body.
  • C++14 generic lambdas use auto parameters and work like function templates.
  • std::function<R(Args...)> stores any callable matching a signature, at the cost of type-erasure overhead.
  • std::bind creates partially-applied callables; in most cases a lambda is clearer.
  • STL algorithms (std::sort, std::copy_if, std::transform) pair naturally with lambdas to produce readable, expressive code.
  • Immediately-invoked lambdas enable complex const initialisation without helper functions.
  • The most dangerous pitfall is a dangling reference capture — always check that the captured variable outlives the lambda.