Question

C++ Lambda Expressions Explained: What They Are and When to Use Them

cpplambdac++11c++-faq

Question

What is a C++ lambda expression? Why does C++ include lambda expressions, what problems do they solve that were difficult or awkward before they were added, and how can a developer benefit from using them?

Please include one or two examples, especially showing when and where lambda expressions are appropriate.

Short Answer

By the end of this page, you will understand what a C++ lambda expression is, why it was added to the language, and when it is a better choice than a regular function or function object. You will also learn how capture lists work, how lambdas are commonly used with standard library algorithms, and what mistakes beginners often make when using them.

Concept

A lambda expression in C++ is a way to create a small anonymous function object directly where you need it.

In simple terms, a lambda lets you write function-like behavior inline instead of defining a separate named function or custom class.

Basic example:

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

Here, add is a callable object created from a lambda.

Why C++ added lambdas

Before lambdas, if you wanted to pass custom behavior to algorithms like std::sort, std::find_if, or std::for_each, you usually had to use one of these:

  • a regular function
  • a pointer to a function
  • a custom functor class with operator()

Those approaches worked, but they had limitations:

  • Regular functions cannot easily use local variables from the surrounding scope.
  • Function pointers are limited and less expressive.
  • Functor classes are powerful, but often verbose for simple one-time logic.

Lambdas solve this by letting you:

  • define behavior at the point of use
  • capture local variables from the surrounding scope
  • keep code shorter and easier to read
  • avoid creating small throwaway classes or helper functions

What problem they solve

The biggest problem lambdas solve is passing custom logic that depends on nearby local state.

For example, suppose you want to count numbers greater than a local variable limit:

int limit = 10;

Before lambdas, it was awkward to pass limit into a comparison function for an algorithm. With a lambda, you can capture it directly:

int count = std::count_if(numbers.begin(), numbers.end(), [limit](int n) {
    return n > limit;
});

That is concise, local, and easy to understand.

Why this matters in real programming

In real code, developers often need short custom actions for:

  • sorting data
  • filtering collections
  • event handlers
  • callbacks
  • thread tasks
  • deferred work
  • custom predicates and transformations

Lambdas make these tasks much cleaner because the logic stays close to the place where it is used.

Important idea: lambdas are function objects

A lambda is not exactly a plain function. It creates a unique unnamed type that behaves like an object with operator().

This matters because:

  • lambdas can store captured values as object state
  • each lambda has its own type
  • they are often more flexible than raw function pointers

So, a lambda is best understood as a compact way to define a callable object inline.

Mental Model

Think of a lambda as a temporary worker you hire for one small job.

  • A regular function is like a permanent employee with a name and a fixed office.
  • A functor class is like hiring someone but first building a full personnel file.
  • A lambda is like saying: "For this one task, right here, do this exact thing."

The capture list is the information you hand to that worker before they start.

  • [x] means: give the worker a copy of x
  • [&x] means: let the worker use the original x
  • [=] means: copy everything needed
  • [&] means: use everything needed by reference

This model helps explain why lambdas are so useful in algorithms: you can send a tiny piece of behavior together with the local data it needs.

Take Quiz

Syntax and Examples

General syntax

[capture](parameters) -> return_type {
    // body
}

In many cases, C++ can infer the return type, so -> return_type is optional.

Small example

#include <iostream>

int main() {
    auto greet = []() {
        std::cout << "Hello from a lambda\n";
    };

    greet();
}

What this does

  • [] is the capture list
  • () is the parameter list
  • the body works like a function body
  • greet stores the lambda object
  • greet() calls it

Example with parameters

#include <iostream>

int main() {
    auto multiply = [](int a, int b) {
        return a * b;
    };

    std::cout << multiply(3, 4) << '\n';
}

This prints:

12

Example with capture

#include <iostream>

int main() {
    int bonus = 5;

    auto addBonus = [bonus](int score) {
        return score + bonus;
    };

    std::cout << addBonus(10) << '\n';
}

Why capture is important

The lambda uses bonus, which exists outside the lambda. Without capture, the lambda cannot access it.

Example with std::sort

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

int main() {
    std::vector<int> numbers = {4, 1, 7, 3, 9};

    std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
        return a > b;
    });

    for (int n : numbers) {
        std::cout << n << ' ';
    }
}

This sorts the numbers in descending order.

Why a lambda fits here

The comparison logic is short and only needed once. Creating a separate named function would add unnecessary distance and boilerplate.

Common capture forms

[x]     // capture x by value
[&x]    // capture x by reference
[=]     // capture used outer variables by value
[&]     // capture used outer variables by reference
[&, x]  // default by reference, but x by value
[=, &x] // default by value, but x by reference

Mutable lambda

By default, captured-by-value variables are treated as read-only inside the lambda. Use mutable if you want to modify the lambda's internal copy.

#include <iostream>

int main() {
    int x = 10;

    auto f = [x]() mutable {
        x += 5;
        std::cout << x << '\n';
    };

    f();
    std::cout << x << '\n';
}

Output:

15
10

The original x remains unchanged because the lambda modified its own copy.

Step by Step Execution

Consider this example:

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

int main() {
    std::vector<int> numbers = {3, 8, 12, 5, 20};
    int limit = 10;

    int count = std::count_if(numbers.begin(), numbers.end(), [limit](int n) {
        return n > limit;
    });

    std::cout << count << '\n';
}

Step-by-step

1. A vector is created

std::vector<int> numbers = {3, 8, 12, 5, 20};

The vector contains 5 values.

2. A local variable is created

int limit = 10;

This value will be used inside the lambda.

3. std::count_if starts scanning the vector

std::count_if(numbers.begin(), numbers.end(), ...)

It checks each element and counts how many satisfy a condition.

4. The lambda is passed as the condition

[limit](int n) {
    return n > limit;
}

This means:

  • capture limit by value
  • take one parameter n
  • return true if n is greater than limit

5. The algorithm calls the lambda for each element

It behaves roughly like this:

  • n = 3 → 3 > 10 is false
  • n = 8 → 8 > 10 is false
  • n = 12 → 12 > 10 is true
  • n = 5 → 5 > 10 is false
  • n = 20 → 20 > 10 is true

6. The count is stored

There are 2 values greater than 10, so:

int count = 2;

7. The result is printed

std::cout << count << '\n';

Output:

2

Key takeaway

The lambda gives std::count_if a custom rule without needing a separate named function. It also uses the local variable limit, which is one of the main reasons lambdas are so useful.

Real World Use Cases

1. Custom sorting

You often need to sort objects by a specific field.

std::sort(users.begin(), users.end(), [](const User& a, const User& b) {
    return a.age < b.age;
});

Useful in:

  • user lists
  • product catalogs
  • leaderboard ranking

2. Filtering data

Lambdas are commonly used with algorithms like std::find_if, std::count_if, and std::remove_if.

auto it = std::find_if(tasks.begin(), tasks.end(), [](const Task& t) {
    return t.completed;
});

Useful in:

  • task apps
  • log analysis
  • data cleanup scripts

3. Callbacks and event handling

A lambda can define a small action to run when something happens.

button.onClick([]() {
    std::cout << "Button clicked\n";
});

Useful in:

  • GUI applications
  • game engines
  • event-driven systems

4. Thread work

Lambdas are often used to define a task for a thread.

#include <thread>

int value = 42;
std::thread worker([value]() {
    std::cout << value << '\n';
});
worker.join();

Useful in:

  • background processing
  • async jobs
  • server tasks

5. Local helper logic

Sometimes a piece of logic is only needed in one function. A lambda avoids polluting the wider namespace.

auto isValid = [](int x) {
    return x >= 0 && x <= 100;
};

Useful in:

  • validation
  • parsing
  • one-off transformations
Take Quiz

Real Codebase Usage

In real projects, lambdas are rarely used just because they are shorter. They are used because they help keep behavior close to where it is needed.

Common patterns

Guard-like checks inside algorithms

auto it = std::find_if(items.begin(), items.end(), [](const Item& item) {
    return !item.name.empty();
});

This keeps simple conditions inline and readable.

Validation rules

auto hasInvalidUser = std::any_of(users.begin(), users.end(), [](const User& user) {
    return user.id <= 0 || user.email.empty();
});

This pattern appears in data validation and API input checks.

Transforming collections

std::vector<int> lengths;
std::transform(words.begin(), words.end(), std::back_inserter(lengths), [](const std::string& word) {
    return static_cast<int>(word.size());
});

This is common in data processing code.

Error handling and deferred actions

Lambdas are often passed into APIs that retry work, schedule tasks, or handle failures.

auto operation = []() {
    // do some work
};

Capturing configuration or context

int threshold = 100;
auto isLarge = [threshold](int value) {
    return value > threshold;
};

This is helpful when business rules depend on local settings.

Good real-code habits

  • Use lambdas for short, local behavior.
  • Prefer them when the logic is only used once.
  • Avoid very long lambdas; extract them into a named function if they become hard to read.
  • Be careful when capturing by reference, especially if the lambda outlives the current scope.

Common Mistakes

1. Forgetting to capture a variable

Broken code:

int limit = 10;
auto f = [](int x) {
    return x > limit;
};

This fails because limit is not available inside the lambda.

Correct version:

int limit = 10;
auto f = [limit](int x) {
    return x > limit;
};

2. Capturing by reference when the lambda lives longer than the variable

Broken idea:

auto makeLambda() {
    int x = 5;
    return [&x]() {
        return x;
    };
}

This is dangerous because x no longer exists after the function returns.

Safer version:

auto makeLambda() {
    int x = 5;
    return [x]() {
        return x;
    };
}

3. Using [&] or [=] too casually

These defaults are convenient, but they can hide what is actually being captured.

Less explicit:

auto f = [&](int value) {
    return value + offset;
};

Clearer when practical:

auto f = [&offset](int value) {
    return value + offset;
};

4. Expecting value capture to modify the original variable

Broken expectation:

int total = 0;
auto f = [total]() mutable {
    total += 10;
};

f();
// total is still 0

The lambda changed its copy, not the original variable.

To change the original:

int total = 0;
auto f = [&total]() {
    total += 10;
};

5. Making lambdas too large

If a lambda grows to many lines, it becomes harder to read.

A good rule:

  • short local logic -> lambda
  • reusable or complex logic -> named function or class

6. Assuming all lambdas are function pointers

Captureless lambdas can often convert to function pointers, but capturing lambdas cannot.

auto a = []() { return 1; };      // may convert
int x = 5;
auto b = [x]() { return x; };     // not a plain function pointer

Comparisons

Lambda vs other ways to provide behavior

ApproachBest forCan use local state easily?BoilerplateReusability
Regular functionReusable logicNoLowHigh
Function pointerSimple callback APIsNoLowMedium
Functor classStateful reusable callable objectsYesHighHigh
LambdaShort local behaviorYesLowMedium

Lambda vs named function

FeatureLambdaNamed function
Defined inlineYesNo
Can capture local variablesYesNo
Good for one-off logicYesSometimes
Good for large reusable logicUsually noYes

Capture by value vs capture by reference

Capture styleSyntaxWhat it meansWhen to use
By value[x]Makes a copy of xWhen you want safety or a snapshot
By reference[&x]Uses the original xWhen changes should affect the original

When not to use a lambda

Avoid a lambda when:

  • the logic is used in many places
  • the code is long or complex
  • the behavior needs a descriptive name
  • lifetime and reference capture would be risky
Take Quiz

Cheat Sheet

Quick syntax

[]() { }
[](int x) { return x * 2; }
[x]() { return x; }
[&x]() { x++; }
[=]() { /* capture used variables by value */ }
[&]() { /* capture used variables by reference */ }
[x]() mutable { x++; }

Parts of a lambda

[capture](parameters) -> return_type {
    // body
}
  • capture: outer variables to bring in
  • parameters: inputs to the lambda
  • return_type: optional if C++ can infer it
  • body: code to run

Common uses

  • std::sort
  • std::find_if
  • std::count_if
  • std::for_each
  • std::transform
  • callbacks
  • threads

Capture rules

  • [x] copies x
  • [&x] references x
  • [=] copies used outer variables
  • [&] references used outer variables
  • mutable lets you modify captured-by-value copies

Good habits

  • Use lambdas for short local logic
  • Prefer explicit captures when clarity matters
  • Avoid returning lambdas that capture dead references
  • Extract complex lambdas into named functions

Edge case to remember

A lambda with captures is not the same as a plain function pointer.

FAQ

What is a lambda expression in C++?

A lambda expression is an anonymous callable object that lets you write function-like logic inline, often right where it is passed to an algorithm or callback.

Why use lambdas instead of regular functions in C++?

Use lambdas when the logic is short, local, and needs access to nearby variables. Regular functions are better for reusable logic that deserves a name.

What does the capture list mean in a C++ lambda?

The capture list controls which outside variables the lambda can use, and whether it gets them by value or by reference.

When should I capture by value vs by reference?

Capture by value when you want a safe copy or snapshot. Capture by reference when the lambda must modify the original variable or avoid copying, but be careful with lifetimes.

Are lambdas faster than normal functions in C++?

Not necessarily, but they are often efficient and can be inlined by the compiler. The main benefit is usually cleaner and more expressive code.

Can a C++ lambda return a value?

Yes. A lambda can return values just like a normal function, and the return type is often inferred automatically.

Can I store a lambda in a variable?

Yes. You can usually store it with auto, or in std::function when you need a common callable wrapper.

When should I avoid using lambdas?

Avoid them when the logic is long, reused in many places, or needs a clear descriptive name.

Take Quiz

Mini Project

Description

Build a small C++ program that manages a list of numbers and uses lambda expressions to process them. This project demonstrates the most common beginner use cases for lambdas: filtering, counting, and sorting with local rules.

Goal

Create a program that counts numbers above a threshold, removes unwanted values, and sorts the remaining values using lambda expressions.

Requirements

  1. Create a std::vector<int> with at least 8 numbers.
  2. Use a lambda with std::count_if to count how many values are greater than a chosen threshold.
  3. Use a lambda with std::remove_if to remove values below a minimum allowed value.
  4. Use a lambda with std::sort to sort the remaining values in descending order.
  5. Print the results after each step.
Take Quiz

Keep learning

Related questions

View more