Lambda Expressions

Reviewed & published by Brayan K

A lambda is a tiny function you write right where you use it — no separate declaration, no name needed. They make standard algorithms like std::sort and std::count_if read like plain English, and they're the everyday glue of modern C++. By the end of this lesson you'll write lambdas with parameters and captures, and pass them to algorithms to filter, sort, and accumulate data.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A lambda is a sticky note with an instruction that you hand to a helper. When you ask std::sort to put a list in order, you can attach a sticky note that says "shorter words first" — that note is the lambda. The capture list is what you write on the back of the note from your own desk: copy a number to use ([value]), or point back at the original so changes are reflected ([&reference]). The helper doesn't need to know your whole program — just the small rule on the note.

📊 Anatomy of a Lambda

[capture](parameters) -> returnType { body }
   |          |             |          |
   |          |             |          +-- the code to run
   |          |             +------------- optional; usually deduced
   |          +--------------------------- inputs, like any function
   +-------------------------------------- which outside variables to use
CaptureMeaning
[]capture nothing
[x]capture x by value (a copy)
[&x]capture x by reference (the original)
[=]capture everything used, by value
[&]capture everything used, by reference
[x, &y]x by value, y by reference

1. Your First Lambdas

The simplest lambda has an empty capture list [], no parameters, and a body. Store it in a variable with auto (each lambda has its own unique compiler-generated type) and call it like a function. Lambdas can take parameters and return values just like named functions — the return type is usually deduced automatically.

#include <iostream>
using namespace std;

int main() {
    // A lambda is an unnamed function written inline.
    // Shape:  [capture](params) { body }
    auto greet = []() {
        cout << "Hello from a lambda!" << endl;
    };
    greet();  // call it like a normal function -> Hello from a lambda!

    // Lambdas can take parameters and return a value:
    auto add = [](int a, int b) {
        return a + b;     // return type is deduced (int here)
    };
    cout << "3 + 4 = " << add(3, 4) << endl;   // 3 + 4 = 7

    // You can even call a lambda immediately:
    int sq = [](int n) { return n * n; }(5);
    cout << "5 squared = " << sq << endl;      // 5 squared = 25
    return 0;
}

// ✅ Expected output:
//    Hello from a lambda!
//    3 + 4 = 7
//    5 squared = 25

2. Captures: by Value vs by Reference

A lambda can use variables from the surrounding scope, but only if you capture them. [factor] takes a copy — a snapshot frozen at the moment the lambda is created. [&total] takes a reference to the original, so the lambda can read and modify the live value. Choosing the right kind of capture is the heart of using lambdas well.

#include <iostream>
using namespace std;

int main() {
    int factor = 10;
    int total  = 0;

    // [factor] captures factor BY VALUE -> the lambda gets a copy.
    auto scale = [factor](int x) { return x * factor; };
    cout << scale(3) << endl;          // 30

    // Changing factor AFTER capture does NOT affect the copy:
    factor = 99;
    cout << scale(3) << endl;          // still 30 (used the old copy)

    // [&total] captures BY REFERENCE -> it touches the original.
    auto addToTotal = [&total](int x) { total += x; };
    addToTotal(5);
    addToTotal(7);
    cout << "total = " << total << endl;  // total = 12
    return 0;
}

// ✅ Expected output:
//    30
//    30
//    total = 12

Your turn. Fill in the capture so the lambda can use the bonus variable by value:

#include <iostream>
using namespace std;

int main() {
    // 🎯 YOUR TURN — make a lambda that adds a fixed bonus.
    int bonus = 50;

    // Capture 'bonus' BY VALUE, take a score, return score + bonus.
    auto withBonus = ___ (int score) { return score + bonus; };
    //                ^^^ 👉 [bonus]

    cout << withBonus(100) << endl;   // ✅ Expected: 150
    cout << withBonus(20)  << endl;   // ✅ Expected: 70
    return 0;
}

🧩 Reorder Challenge

These lines define a lambda that doubles a number and use it. Put them in the correct order:

A.     cout << twice(21) << endl;
B.     auto twice = [](int n) { return n * 2; };
C. int main() {
D.     return 0;
E. }

Correct order: C → B → A → D → E

Open main (C), define the twice lambda (B), call it — printing 42 (A), then return 0; (D) and close the brace (E). A lambda must be defined before it's called.

3. Lambdas with std::sort

The most common use of a lambda is as a comparator for std::sort. You pass a lambda taking two elements that returns true when the first should come before the second. This lets you sort by any rule you like — by length, by a struct field, descending, whatever — without writing a separate function.

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;

int main() {
    vector<string> names = {"Charlie", "Al", "Bob", "Dan"};

    // Sort by LENGTH using a lambda as the comparator.
    // It must return true when 'a' should come BEFORE 'b'.
    sort(names.begin(), names.end(),
         [](const string& a, const string& b) {
             return a.size() < b.size();   // shorter strings first
         });

    for (const string& n : names) cout << n << " ";
    cout << endl;   // Al Bob Dan Charlie
    return 0;
}

// ✅ Expected output:
//    Al Bob Dan Charlie

4. count_if, for_each, find_if

Lambdas shine with the standard algorithms. A lambda returning bool is called a predicate: count_if counts how many elements satisfy it, find_if returns the first that does. for_each runs an action on every element — capture a variable by reference to accumulate a result.

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {4, 7, 2, 9, 5, 1, 8};

    // count_if: how many satisfy a predicate (a lambda returning bool)?
    int evens = count_if(nums.begin(), nums.end(),
                         [](int n) { return n % 2 == 0; });
    cout << "even count: " << evens << endl;     // even count: 3

    // for_each: run an action on every element.
    int sum = 0;
    for_each(nums.begin(), nums.end(),
             [&sum](int n) { sum += n; });
    cout << "sum: " << sum << endl;              // sum: 36

    // find_if: first element matching a rule (returns an iterator).
    auto it = find_if(nums.begin(), nums.end(),
                      [](int n) { return n > 6; });
    if (it != nums.end()) cout << "first >6: " << *it << endl; // first >6: 7
    return 0;
}

// ✅ Expected output:
//    even count: 3
//    sum: 36
//    first >6: 7

5. mutable: Lambdas That Remember

By default, variables captured by value are const inside the lambda — the body can read its copies but not change them. Add the mutable keyword after the parameter list to allow changes to those copies. Combined with an init-capture like [count = 0], this lets a lambda keep its own state between calls, behaving like a tiny stateful object.

#include <iostream>
using namespace std;

int main() {
    // A lambda can keep STATE across calls by capturing by value
    // and marking itself 'mutable' so it may change its own copy.
    auto counter = [count = 0]() mutable {
        return ++count;   // the captured copy persists between calls
    };

    cout << counter() << endl;  // 1
    cout << counter() << endl;  // 2
    cout << counter() << endl;  // 3

    // Without 'mutable', the body could not modify 'count' (it's const).
    return 0;
}

// ✅ Expected output:
//    1
//    2
//    3

🔎 Deep Dive: what a lambda really is

A lambda is syntactic sugar for an unnamed function object (a "functor"): the compiler generates a hidden struct with an operator(), and the captures become its data members. That's why each lambda has a unique type and why auto is the natural way to hold one.

When you need a single named type that can hold any callable with a given signature — for example to store different lambdas in one container — use std::function, at a small runtime cost:

#include <functional>
std::function<int(int)> op = [](int x) { return x + 1; };
op = [](int x) { return x * x; };   // reassign to a different lambda
cout << op(5);                      // 25

Pro Tips

🧠 Quick Recall

Predict the output before revealing the answer.

int k = 5;
auto f = [k](int n) { return n + k; };
k = 100;
cout << f(1);

6 — k was captured by value (a copy of 5). Changing k to 100 afterward doesn't affect the lambda's copy: 1 + 5 = 6.

2. What is the final value of total?

int total = 0;
auto add = [&total](int n) { total += n; };
add(3); add(4);
cout << total;

7 — total is captured by reference, so both calls modify the original: 0 + 3 + 4 = 7.

3. What does this print?

auto next = [i = 0]() mutable { return i++; };
cout << next() << next() << next();

012 — post-increment returns the old value then increments the captured copy: 0, then 1, then 2.

Common Errors (and the fix)

📋 Quick Reference

TaskCode
No-capture lambdaauto f = [](int x){ return x+1; };
Capture by value[k](int x){ return x*k; }
Capture by reference[&sum](int x){ sum += x; }
Mutable / stateful[c=0]() mutable { return ++c; }
Sort with comparatorsort(b, e, [](auto a, auto b){...})
Count matchingcount_if(b, e, pred)
Store any callablefunction<int(int)> g = ...;

Mini-Challenge: Filter and Sum

Combine a predicate and a reference capture: count the expensive items and sum only those. The comment outline keeps you on track.

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main() {
    // 🎯 MINI-CHALLENGE: Filter and sum
    // Given the prices below:
    // 1. Use count_if with a lambda to count how many prices are > 20.
    // 2. Use for_each (or a loop) with a lambda capturing a 'total'
    //    by reference to sum ONLY the prices > 20.
    // 3. Print the count and the total.
    //
    vector<double> prices = {12.5, 30.0, 8.0, 45.5, 22.0, 5.0};
    //
    // ✅ Example output:
    //    expensive items: 3
    //    expensive total: 97.5

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What is the basic syntax of a C++ lambda expression?

  • [capture](parameters) { body }
  • lambda(parameters) -> body
  • function[capture](parameters)
  • { body }(parameters)[capture]

Answer: [capture](parameters) { body }. A lambda is written [capture](parameters) { body }, with an optional -> returnType after the parameters.

What does the capture [x] do?

  • Captures x by reference
  • Captures x by value (a copy)
  • Captures everything by value
  • Captures nothing

Answer: Captures x by value (a copy). [x] captures x by value — the lambda gets its own copy, frozen at creation time. [&x] would capture by reference.

What prints? int k = 5; auto f = [k](int n){ return n + k; }; k = 100; cout << f(1);

  • 101
  • 6
  • 105
  • 1

Answer: 6. k was captured by value (a copy of 5). Changing k to 100 afterward does not affect the copy, so f(1) = 1 + 5 = 6.

What is the final value of total? int total = 0; auto add = [&total](int n){ total += n; }; add(3); add(4);

  • 0
  • 3
  • 7
  • 12

Answer: 7. total is captured by reference, so both calls modify the original: 0 + 3 + 4 = 7.

By default, can a lambda modify a variable it captured by value inside its body?

  • Yes, always
  • No — the copy is const unless you add mutable
  • Only if it is an int
  • Only with [&] capture

Answer: No — the copy is const unless you add mutable. Value captures are const inside the body by default. Adding mutable after the parameter list lets the lambda change its own copies.

Which keyword is the natural way to store a lambda in a variable?

  • auto
  • lambda
  • var
  • void

Answer: auto. Each lambda has its own unique compiler-generated type, so auto is the natural way to hold one.

A lambda passed to std::sort as a comparator should return true when:

  • The two elements are equal
  • The first element should come before the second
  • The first element is greater
  • Sorting is finished

Answer: The first element should come before the second. A sort comparator must be a strict less-than: return true when 'a' should come before 'b' (e.g. a.size() < b.size()).

What does std::count_if(begin, end, pred) return?

  • An iterator to the first match
  • true or false
  • How many elements satisfy the predicate
  • The sum of matching elements

Answer: How many elements satisfy the predicate. count_if counts how many elements satisfy the predicate. find_if returns an iterator to the first match; for_each runs an action on each.

What is a lambda really, under the hood?

  • A macro
  • An unnamed function object (functor) with operator() and captures as data members
  • A plain function pointer
  • A preprocessor directive

Answer: An unnamed function object (functor) with operator() and captures as data members. The compiler generates a hidden struct with operator(); the captures become its data members — which is why each lambda has a unique type.

To store any callable with signature int(int) in one named type, you would use:

  • std::function<int(int)>
  • auto
  • int*
  • std::vector<int>

Answer: std::function<int(int)>. std::function<int(int)> can hold any callable matching that signature (at a small runtime cost), letting you reassign different lambdas.

Continue this course

Frequently asked questions

What is a lambda expression in C++?

A lambda is a small, unnamed function you can write inline, right where you need it. The basic shape is [capture](parameters) { body }. They are most often passed to algorithms like std::sort, std::for_each, or std::count_if to describe a custom rule.

What does the [ ] capture list do?

It controls which surrounding variables the lambda can see and how. [] captures nothing, [x] captures x by value (a copy), [&x] captures x by reference (the original), [=] captures everything used by value, and [&] captures everything by reference.

What is the difference between [=] and [&]?

[=] copies the variables it uses, so the lambda has its own snapshot that won't change if the originals do. [&] refers to the originals, so it sees and can modify the current values. Prefer capturing exactly what you need rather than [=] or [&] blanket captures.

Why would I use mutable on a lambda?

By default a lambda's body cannot modify variables captured by value (the copies are const). Adding the mutable keyword after the parameter list lets the body change those copies. The originals outside are still untouched, because only the copies change.

How do I store a lambda in a variable?

Use auto to let the compiler deduce the lambda's unique type: auto add = [](int a, int b){ return a + b; };. If you need a named, assignable function type you can store it in std::function<int(int,int)>, at a small runtime cost.

Related lessons