Skip to content

Lambda Expressions — Capture, Parameters, Return Type, Generic Lambdas, IIFE

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Lambda Expressions. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ lambda expressions create anonymous function objects at the point of use, capturing surrounding variables and supporting inline definition of callbacks, predicates, and short-lived functions.

What You'll Learn

You will write lambda expressions with various capture modes, use lambdas with STL algorithms and as callbacks, create generic lambdas (C++14) with auto parameters, understand capture lifetime and dangling references, and use IIFE (immediately invoked function expressions) for scoped initialization.

Why It Matters

Lambdas eliminate the boilerplate of defining separate functor classes for simple operations. Instead of writing a 5-line struct with operator(), you write a one-line lambda. They are essential for STL algorithms, parallel execution, asynchronous code, and callback-based APIs. Every modern C++ codebase uses lambdas extensively.

Learning Path

graph LR
    A["49: Type Traits & Metaprogramming"] --> B["50: Lambda Expressions"]
    B --> C["51: auto & decltype"]
    C --> D["52: Move Semantics"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic Lambda Syntax

A lambda consists of capture [], parameters (), return type ->, and body {}.

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

int main() {
    // Simplest lambda: no capture, no parameters
    auto greet = [] {
        std::cout << "Hello from lambda!\n";
    };
    greet();  // Hello from lambda!

    // Lambda with parameters
    auto add = [](int a, int b) -> int {
        return a + b;
    };
    std::cout << add(3, 4) << "\n";  // 7

    // Return type can be deduced (usually)
    auto mul = [](int a, int b) { return a * b; };
    std::cout << mul(5, 6) << "\n";  // 30

    // Using with STL algorithms
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};

    // Count evens with lambda
    auto evenCount = std::count_if(v.begin(), v.end(),
        [](int x) { return x % 2 == 0; });
    std::cout << "Evens: " << evenCount << "\n";  // 4

    // Sort descending
    std::sort(v.begin(), v.end(),
        [](int a, int b) { return a > b; });

    for (int x : v) std::cout << x << " ";
    std::cout << "\n";  // 8 7 6 5 4 3 2 1
}

Capture Modes

Lambdas can capture local variables by value or by reference.

#include <iostream>

int main() {
    int x = 10;
    int y = 20;

    // Capture by value (copy)
    auto byValue = [x]() {
        // x = 5;  // Error: x is const (captured by value)
        return x + 1;
    };
    std::cout << byValue() << "\n";  // 11
    std::cout << "x unchanged: " << x << "\n";  // 10

    // Capture by reference
    auto byRef = [&x]() {
        x = 99;  // Modifies original
        return x;
    };
    std::cout << byRef() << "\n";  // 99
    std::cout << "x modified: " << x << "\n";  // 99

    // Mixed capture
    auto mixed = [x, &y]() {
        y = x + y;
        return y;
    };

    // Default captures
    auto defaultCopy = [=]() {  // Capture all by value
        return x + y;
    };

    auto defaultRef = [&]() {   // Capture all by reference
        x = 0;
        y = 0;
    };

    // Init capture (C++14): move/capture expressions
    auto init = [z = x + y]() {  // z is initialized at lambda creation
        return z;
    };
    std::cout << init() << "\n";  // 99 (x was modified above)

    // Move unique_ptr into lambda
    auto ptr = std::make_unique<int>(42);
    auto moveCapture = [p = std::move(ptr)]() {
        return *p;
    };
    std::cout << moveCapture() << "\n";  // 42
    // ptr is now null (moved into lambda)
}

Mutable Lambdas

By default, operator() on a lambda is const. Use mutable to modify captured values.

#include <iostream>

int main() {
    int count = 0;

    // Without mutable: captures are const (read-only)
    auto printer = [count]() mutable {
        ++count;  // OK with mutable — modifies the lambda's copy
        std::cout << "Called " << count << " times\n";
    };

    printer();  // Called 1 times
    printer();  // Called 2 times
    std::cout << "Original count: " << count << "\n";  // 0 (unchanged)

    // Mutable with reference capture modifies original
    auto refCount = [&count]() mutable {
        ++count;  // Modifies original through reference
    };
    refCount();
    std::cout << "Original after ref: " << count << "\n";  // 1
}

Generic Lambdas (C++14)

Parameters can be auto, making lambdas generic — equivalent to function templates.

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

int main() {
    // Generic lambda: T is deduced per call
    auto print = [](const auto& value) {
        std::cout << value << "\n";
    };

    print(42);              // 42
    print(3.14);            // 3.14
    print("hello");         // hello
    print(std::string("world")); // world

    // Generic lambda with two parameters
    auto add = [](const auto& a, const auto& b) {
        return a + b;
    };

    std::cout << add(3, 4) << "\n";          // 7
    std::cout << add(1.5, 2.5) << "\n";      // 4.0
    std::cout << add(std::string("a"), "b") << "\n";  // ab

    // Use with any container
    auto sortAndPrint = [](auto& container) {
        std::sort(container.begin(), container.end());
        for (const auto& elem : container) {
            std::cout << elem << " ";
        }
        std::cout << "\n";
    };

    std::vector<int> vi = {3, 1, 4, 1, 5};
    sortAndPrint(vi);  // 1 1 3 4 5

    std::vector<std::string> vs = {"banana", "apple", "cherry"};
    sortAndPrint(vs);  // apple banana cherry

    // Templated lambda (C++20): explicit template parameters
    auto templated = []<typename T>(const std::vector<T>& vec) {
        return vec.size();
    };
    std::cout << templated(vi) << "\n";  // 5
}

IIFE — Immediately Invoked Function Expression

Lambdas can be called immediately after definition, useful for scoped initialization.

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

class ExpensiveResource {
public:
    ExpensiveResource() { std::cout << "Created\n"; }
    ~ExpensiveResource() { std::cout << "Destroyed\n"; }
    void use() { std::cout << "Using\n"; }
};

int main() {
    // IIFE: create + call immediately
    // Useful for initializing const values that need computation
    const auto data = []() {
        std::vector<int> temp(1000);
        std::generate(temp.begin(), temp.end(), [n = 0]() mutable { return n++; });
        // Complex initialization logic here
        return temp;
    }();  // Called immediately

    std::cout << "Data size: " << data.size() << "\n";  // 1000

    // IIFE for scoped resource management
    {
        auto resource = []() {
            auto res = std::make_unique<ExpensiveResource>();
            res->use();
            return res;
        }();  // Created, used
        std::cout << "Inside scope\n";
    }  // Destroyed when unique_ptr goes out of scope
    std::cout << "Outside scope\n";
}

Capturing this and Member Variables

#include <iostream>

class Counter {
    int count_ = 0;
public:
    void increment() { ++count_; }

    auto getLambda() {
        // Capture this by value (copies the pointer)
        return [this]() { return count_; };

        // Capture *this by copy (C++17) — captures entire object
        // return [*this]() { return count_; };
    }

    auto getMutableLambda() {
        return [this]() mutable {
            return ++count_;
        };
    }
};

int main() {
    Counter c;
    c.increment();
    c.increment();

    auto lambda = c.getLambda();
    std::cout << lambda() << "\n";  // 2

    auto mutableLambda = c.getMutableLambda();
    std::cout << mutableLambda() << "\n";  // 3
    std::cout << mutableLambda() << "\n";  // 4

    // *this capture (C++17): captures the object by value
    // Any modifications inside the lambda do NOT affect the original
}

Lambda to Function Pointer

Non-capturing lambdas can convert to function pointers.

#include <iostream>

void execute(void(*func)()) {
    func();
}

int main() {
    // Non-capturing lambda: converts to function pointer
    auto lambda = []() {
        std::cout << "Called via function pointer\n";
    };

    execute(lambda);  // OK: converts automatically

    // execute([]() { /* ... */ });  // Same

    // Capturing lambda: cannot convert to function pointer
    int x = 5;
    auto capturing = [x]() {
        std::cout << x << "\n";
    };
    // execute(capturing);  // Error: cannot convert
}

Common Mistakes

Mistake 1: Dangling references in capture

auto badLambda() {
    int x = 5;
    return [&x]() { return x; };  // x is destroyed when function returns!
}

Capture by value [x] or use std::shared_ptr for extending lifetimes.

Mistake 2: Modifying captured values without mutable

int count = 0;
auto lambda = [count]() { ++count; };  // Error: count is const

Add mutable keyword: [count]() mutable { ++count; }.

Mistake 3: Capturing all by reference in long-lived lambdas

auto lambda = [&]() { /* ... */ };  // Captures everything by reference

Default reference capture can cause dangling references. Be explicit.

Mistake 4: Overlooking that each lambda has a unique type

auto a = []{};
auto b = []{};
// a and b have different types!
// cannot assign a = b

Mistake 5: Recursive lambda is tricky

auto fib = [](int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);  // Error: fib not yet defined
};

Use std::function or Y-combinator pattern for recursive lambdas.

Practice Questions

  1. What is the output?
int x = 5;
auto l = [&x]() { return ++x; };
std::cout << l() << " " << x;

Answer: 6 6 — reference capture modifies the original.

  1. What does mutable do in a lambda? Answer: Allows the lambda to modify its value-captured members. Without mutable, captured values are const.

  2. Write a lambda that computes the sum of a vector.

auto sum = [](const std::vector<int>& v) {
    int total = 0;
    for (int x : v) total += x;
    return total;
};
  1. Can a capturing lambda be converted to a function pointer? Answer: No. Only non-capturing lambdas (empty []) can convert to function pointers.

  2. What is an IIFE and why use it? Answer: Immediately Invoked Function Expression — a lambda defined and called in one step. Used for initializing const values with complex logic.

FAQ

What is a lambda expression in C++

A lambda expression is an anonymous function object defined inline. It consists of a capture list, parameter list, optional return type, and function body.

How do captures work in lambdas

Captures copy or reference variables from the surrounding scope. [x] captures by value, [&x] by reference, [=] captures all by value, [&] captures all by reference.

What are generic lambdas in C++14

Generic lambdas use auto parameters, making them templates. They work with any type that satisfies the operations used in the body.

What happens when a lambda captures a dangling reference

Accessing a dangling reference is undefined behavior — the captured reference points to a destroyed object. Always ensure capture lifetimes exceed the lambda's lifetime.

How are lambdas different from std::function

Lambdas have unique types and are usually stack-allocated. std::function is a type-erased wrapper with heap allocation overhead. Use auto for lambdas, std::function only when storing.

Mini Project

Build a simple pipeline system using lambdas — a series of transformations applied to data:

#include <iostream>
#include <vector>
#include <functional>

// Pipeline class that composes lambdas
class Pipeline {
    // Your implementation here
};

int main() {
    Pipeline p;
    p.then([](int x) { return x * 2; })
     .then([](int x) { return x + 1; })
     .then([](int x) { return x * x; });

    std::cout << p.execute(5) << "\n";   // ((5*2)+1)^2 = 121
    std::cout << p.execute(0) << "\n";   // ((0*2)+1)^2 = 1
    std::cout << p.execute(-3) << "\n";  // ((-3*2)+1)^2 = 25
}

This project demonstrates how C++ lambdas enable functional-style composition, similar to how Java uses method references and lambda chains in streams.

What's Next

You now write concise lambdas — the most used C++11 feature. Next, you will learn auto and decltype, the type deduction mechanisms that make generic code cleaner and more maintainable.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro