Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 1 of 5

You have been writing lambdas since Module 13 without them being explained:

std::count_if(v.begin(), v.end(), [](int x) { return x > 5; });

That [](int x) { return x > 5; } is a lambda — a function defined at the point of use, with no name.

The anatomy

[captures](parameters) -> return_type { body }
 ^^^^^^^^  ^^^^^^^^^^     ^^^^^^^^^^^   ^^^^
 required   optional       optional     required

Only the capture list and the body are required. The smallest legal lambda is []{}.

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

int main() {
    std::vector<int> v = {5, 2, 8, 1, 9};

    // The anatomy: [captures](parameters) -> return_type { body }
    auto is_big = [](int x) { return x > 4; };
    std::cout << "count > 4: " << std::count_if(v.begin(), v.end(), is_big) << "\n";

    // Return type is deduced; write it explicitly when the deduction is wrong.
    auto half = [](int x) -> double { return x / 2.0; };
    std::cout << "half of 5: " << half(5) << "\n";

    // Called immediately — useful for initialising a const from several steps.
    const int total = [&v] {
        int sum = 0;
        for (int x : v) sum += x;
        return sum;
    }();
    std::cout << "total: " << total << "\n";

    // A lambda is an object with a type only the compiler can name.
    std::cout << "sizeof empty lambda: " << sizeof(is_big) << "\n";

    int threshold = 4;
    auto captures_one = [threshold](int x) { return x > threshold; };
    std::cout << "sizeof capturing one int: " << sizeof(captures_one) << "\n";

    // Generic lambda (C++14): auto parameters make it a template.
    auto print = [](const auto& value) { std::cout << value << " "; };
    print(42);
    print(3.5);
    print(std::string("text"));
    std::cout << "\n";
    return 0;
}
count > 4: 3
half of 5: 2.5
total: 25
sizeof empty lambda: 1
sizeof capturing one int: 4
42 3.5 text 

Four things there are worth naming.

The return type is deduced from the return statement. half needs the explicit -> double because without it the deduction is still double — but write it when you want to be sure, or when there are several return statements that must agree.

A lambda called immediately — [&v] { ... }() — lets you initialise a const from a multi-step computation. Without it you would need a non-const variable, or a named function. This is the immediately-invoked lambda, and it is the cleanest way to make something const that takes work to compute.

sizeof an empty lambda is 1, not 0, because every object must have a distinct address. Capture one int and it becomes 4. A lambda's size is the size of what it captures — which is the clue to what it really is.

A generic lambda (C++14) uses auto parameters. print accepts an int, a double, and a std::string, because auto parameters make operator() a template (Module 12).

What it compiles to

A lambda is not a special language construct at run time. It is a struct with operator():

#include <iostream>
#include <string>

// What a lambda actually compiles to: an unnamed struct with operator().
struct IsAboveEquivalent {
    int threshold;                                    // the capture, as a member
    bool operator()(int x) const { return x > threshold; }
};

int main() {
    int threshold = 4;

    auto lambda = [threshold](int x) { return x > threshold; };
    IsAboveEquivalent functor{threshold};

    std::cout << std::boolalpha;
    std::cout << "lambda(5):  " << lambda(5) << "\n";
    std::cout << "functor(5): " << functor(5) << "\n";
    std::cout << "same size:  " << (sizeof(lambda) == sizeof(functor)) << "\n";

    // The lambda's operator() is const by default, so this would not compile:
    //   auto counter = [threshold]() { ++threshold; };
    // unless marked mutable:
    auto counter = [n = 0]() mutable { return ++n; };
    std::cout << counter() << " " << counter() << " " << counter() << "\n";

    // mutable modifies the lambda's OWN copy. The original is untouched.
    int outside = 100;
    auto by_value = [outside]() mutable { outside += 1; return outside; };
    std::cout << "inside: " << by_value() << ", outside still: " << outside << "\n";
    return 0;
}
lambda(5):  true
functor(5): true
same size:  true
1 2 3
inside: 101, outside still: 100

The lambda and the hand-written struct behave identically and are the same size. That is because the compiler writes that struct for you — the standard calls it a closure type.

Everything else about lambdas follows from this one fact:

  • Captures are members. That is why sizeof grows with them.
  • operator() is const by default, so a by-value capture cannot be modified without mutable.
  • The type has no name you can write. Only auto (or a template parameter, or std::function) can hold it.
  • Every lambda has a distinct type, even two with identical text. decltype(a) == decltype(b) is false.

mutable

By default the generated operator() is const, so a by-value capture is read-only. mutable removes the const:

// auto counter = [n = 0]() mutable { return ++n; };

counter now has state that persists between calls — a stateful callable. The output 1 2 3 shows it counting.

But read the last two lines of the output again. by_value() returned 101 while outside stayed 100. mutable lets the lambda modify its own copy, not the original. If you wanted the original changed, you needed a reference capture, which is lesson 14.2.

That is also why a mutable lambda passed to an algorithm is a trap: the algorithm may copy it (lesson 13.8), and each copy has its own counter.

Where the -> type is needed

Return type deduction usually works. It fails in one common case:

// auto bad = [](bool flag) {
//     if (flag) return 1;        // deduces int
//     else return 2.5;           // deduces double — error, inconsistent
// };
//
// auto good = [](bool flag) -> double {
//     if (flag) return 1;        // converted to double
//     else return 2.5;
// };

The other case is when you want a reference return, since deduction strips it:

// auto by_value = [](std::vector<int>& v) { return v[0]; };        // returns int
// auto by_ref    = [](std::vector<int>& v) -> int& { return v[0]; }; // returns int&

Lambdas in the wild

The three shapes you will write most:

// A predicate for an algorithm — the most common by far.
// std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
//     return a.score > b.score;
// });
//
// A small transformation.
// std::transform(in.begin(), in.end(), out.begin(),
//                [](int x) { return x * x; });
//
// A callback stored for later — needs std::function or a template (lesson 14.4).
// button.on_click([this] { this->save(); });

When not to use one

When it needs a name. A lambda assigned to a variable and used in three places wants to be a named function. Lambdas earn their keep by being local — defined where used, once.

When it is long. More than about five lines inside an algorithm call and the call site becomes unreadable. Extract a named function.

When you need it before it is defined. A lambda cannot call itself by name (there is no name). Recursion needs a named function, or a std::function, or C++23's deducing this.

Try it

Write lambdas for: squaring a number, checking if a string is longer than n characters, comparing two records by a chosen field, and a counter that returns 1, 2, 3 on successive calls. Then determine sizeof each and explain the differences.

Checkpoint

  • A lambda is [captures](params) -> ret { body }; only the captures and body are required.
  • It compiles to an unnamed struct with operator() — the closure type.
  • Captures are members, which is why sizeof grows with them; an empty lambda is 1 byte.
  • operator() is const by default; mutable removes that.
  • mutable modifies the lambda's own copy, never the original variable.
  • Every lambda has a distinct type, even two with identical text.
  • The type has no spelling — hold it with auto, a template parameter, or std::function.
  • Write -> type when several returns disagree, or when you need a reference return.
  • An immediately-invoked lambda []{ ... }() initialises a const from a multi-step computation.
  • auto parameters (C++14) make a generic lambda — operator() becomes a template.
  • Extract a named function when the lambda needs a name, exceeds a few lines, or must recurse.

Questions about this lesson

Log in to ask a question.