Skip to content
C++ Better Explained
Go back
C++ Lambda Functions Explained: A Beginner's Guide
Edit page

C++ Lambda Functions Explained: A Beginner’s Guide

Imagine you’re sorting a list of students by grade. You need to tell std::sort how to compare two students. The traditional approach is to write a separate comparison function. That works, but you end up with a small helper function sitting far away from the sort call, and you have to scroll back and forth to understand the code.

Lambdas solve this by letting you write that comparison function right where it’s needed — inline, as an anonymous (nameless) function. No scrolling required.

C++ lambdas were introduced in C++11 and have become one of the most useful modern features of the language. This tutorial explains how they work from scratch.


Your First Lambda

Here’s the simplest possible lambda:

#include <iostream>
using namespace std;

int main() {
    auto greet = []() {
        cout << "Hello from a lambda!" << endl;
    };

    greet();  // Call it like a function
    return 0;
}

Breaking it down:

Output:

Hello from a lambda!

Lambda Syntax

The full syntax of a C++ lambda:

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

Most of the time you don’t need to write -> return_type — the compiler figures it out. So in practice it’s usually:

[capture](parameters) {
    body
}

Parameters and Return Values

Lambdas work just like regular functions — they accept parameters and return values:

#include <iostream>
using namespace std;

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

    cout << add(3, 4) << endl;  // 7
    return 0;
}

The compiler deduces that add returns int because a + b is an int.

You can be explicit about the return type if needed:

auto divide = [](double a, double b) -> double {
    return a / b;
};

The Capture Clause []

This is where lambdas get interesting. The capture clause lets the lambda use variables from the surrounding code — the outer scope.

#include <iostream>
using namespace std;

int main() {
    int multiplier = 5;

    auto times = [multiplier](int x) {
        return x * multiplier;  // Uses the outer variable
    };

    cout << times(3) << endl;  // 15
    return 0;
}

Without the capture, multiplier would be invisible inside the lambda body.

Capture Options

SyntaxMeaning
[]Capture nothing
[x]Capture x by value (copy)
[&x]Capture x by reference
[=]Capture all used variables by value
[&]Capture all used variables by reference
[=, &x]Everything by value, but x by reference

Capture by value ([x]) copies the variable at the moment the lambda is created. The lambda gets its own private copy.

Capture by reference ([&x]) means the lambda uses the actual variable. Changes inside the lambda affect the original.

int count = 0;

auto increment = [&count]() {
    count++;  // Modifies the original count
};

increment();
increment();
cout << count << endl;  // 2

Lambdas with STL Algorithms

This is where lambdas shine. The C++ standard library has algorithms like sort, find_if, and for_each that accept a function to customise their behaviour. Lambdas are perfect here.

Sorting

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

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

    // Sort ascending
    sort(nums.begin(), nums.end(), [](int a, int b) {
        return a < b;
    });

    for (int n : nums) cout << n << " ";
    cout << endl;  // 1 2 3 5 8 9
    return 0;
}

Without a lambda, you’d write a separate bool compare(int a, int b) function. With the lambda, the comparison logic sits right next to the sort.

Filtering with find_if

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

int main() {
    vector<int> nums = {3, 7, 2, 9, 4};
    int threshold = 6;

    auto it = find_if(nums.begin(), nums.end(), [threshold](int n) {
        return n > threshold;
    });

    if (it != nums.end()) {
        cout << "First number > " << threshold << ": " << *it << endl;  // 7
    }
    return 0;
}

Iterating with for_each

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

int main() {
    vector<string> words = {"hello", "world", "cpp"};

    for_each(words.begin(), words.end(), [](const string& word) {
        cout << word << " ";
    });
    cout << endl;  // hello world cpp
    return 0;
}
If you're looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you're a complete beginner or looking to solidify your understanding. Just $19.

Sorting Structs with Lambdas

Lambdas are incredibly useful when sorting collections of objects:

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

struct Student {
    string name;
    int grade;
};

int main() {
    vector<Student> students = {
        {"Alice", 88},
        {"Bob", 95},
        {"Charlie", 72}
    };

    // Sort by grade, highest first
    sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
        return a.grade > b.grade;
    });

    for (const auto& s : students) {
        cout << s.name << ": " << s.grade << endl;
    }
    return 0;
}

Output:

Bob: 95
Alice: 88
Charlie: 72

No separate compare function needed. The sorting logic lives right where it’s used.


Lambdas as Function Parameters

You can write functions that accept lambdas as parameters using templates or std::function:

#include <iostream>
#include <functional>
using namespace std;

void applyTwice(int x, function<int(int)> f) {
    cout << f(f(x)) << endl;
}

int main() {
    applyTwice(3, [](int n) { return n * 2; });  // 12
    applyTwice(5, [](int n) { return n + 10; }); // 25
    return 0;
}

std::function<int(int)> means “a callable that takes an int and returns an int”. Any lambda matching that signature works.


Immediately Invoked Lambdas

Sometimes you want to run a lambda right away without storing it in a variable:

int result = [](int a, int b) { return a + b; }(10, 20);
cout << result << endl;  // 30

This looks unusual but is occasionally handy for complex initialisation logic.


Common Mistakes

Capturing by value when you need to modify the original
If you need the lambda to change a variable in the outer scope, capture by reference ([&x]), not by value ([x]).

Dangling references
If you store a lambda that captures by reference, and the original variable goes out of scope, you have a dangling reference — undefined behaviour. Be careful with long-lived lambdas.

Forgetting the capture clause entirely
If your lambda uses outer variables but [] is empty, you’ll get a compilation error. The compiler will tell you exactly which variable wasn’t captured.



Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19


Edit page
Share this post on:

Previous Post
C++ Convert int to string (and string to int): Complete Guide
Next Post
C++ Operator Overloading: A Beginner's Guide