C++ Lambda Functions: The Anonymous Tool for Simplifying Code

Today marks the 80th anniversary of the victory of the Chinese People’s Anti-Japanese War and the World Anti-Fascist War. Everyone must have seen the morning’s military parade, where rows of new weapons showcased the growing strength of our country. As programmers and developers, we should continuously improve ourselves, as doing our essential work contributes to the development of our nation.

Alright, back to the main topic. Today we will learn about lambda functions in C++. In our daily C++ programming, we often have to define a functor just for a simple sorting logic. Additionally, when using STL algorithms, we frequently need to insert a function pointer, even if that function is only a few lines long, we still have to write the declaration and prototype, which can be quite tedious. This is the best time to use lambda functions.

The Background of C++ “Syntactic Sugar” Birth

Before the C++11 standard, when dealing with “temporary function logic” in C++, we often faced two awkward choices:

  1. Function Pointers can pass function addresses but cannot capture external variables, and type conversion is complex, leading to poor readability when used in STL algorithms.
  1. Functors require a separate class/struct definition, even if the logic is only one or two lines, we still have to write the complete class structure, resulting in scattered and redundant code.

For example, suppose we want to sort a vector<int> according to the rule of “odds first, evens last”. Using a functor would require the following code:

#include <vector>#include <algorithm>// Define a functorstruct OddFirst{    bool operator()(int a, int b) const    {        // Return true for odd (to be placed first), false for even        return (a % 2 == 1) && (b % 2 == 0);    }};int main(){    std::vector<int> nums = {3,1,4,1,5,9,2,6};    // Sort using the functor    std::sort(nums.begin(), nums.end(), OddFirst());    return 0;}

In this code, the OddFirst functor only serves the sorting logic of sort, yet it requires a separate definition, making it particularly bulky. Such one-time code can lead to serious code redundancy and reduce readability. After writing similar code multiple times, even naming becomes a challenge.

To solve this problem, C++11 introduced lambda functions— an anonymous function that can be directly embedded into scenarios requiring function objects while supporting the capture of external variables. After rewriting the above example using a lambda, the code becomes much cleaner:

#include <vector>#include <algorithm>int main(){    std::vector<int> nums = {3,1,4,1,5,9,2,6};    // Directly embed the lambda function, logic is the same as the functor    std::sort(nums.begin(), nums.end(),         [](int a, int b) { return (a%2==1) && (b%2==0); }    );    return 0;}

As we can see, the lambda function tightly integrates the “sorting logic” with the “sorting operation”, eliminating the need for an additional class definition, significantly improving both code readability and conciseness. This is the core value of lambda functions:Encapsulating temporary logic with anonymous functions reduces code redundancy and enhances development efficiency..

I believe that C++ lambda functions are inspired by other modern languages like Python, but they are more powerful.

The syntax of lambda functions is as follows

[capture](parameters) -> return_type { function_body };

The meanings of each part are as follows:

  • <span>[capture]</span> Capture list, used to specify which external variables the lambda expression can use.
  • <span>(parameters)</span> Parameter list, similar to ordinary functions, defines the parameters of the lambda expression.
  • <span>-> return_type</span> (optional): Specifies the return type, which can be omitted if it can be automatically deduced.
  • <span>{}</span> Function body, defines the logic of the lambda expression.

Common Uses of C++ Lambda

1. Used with STL algorithms: Simplifying sorting, traversal, and other operations (most common)

STL algorithms (such as sort, for_each, find_if) usually require a “function object” as a parameter, and lambda functions can be directly embedded, avoiding the need to define a separate functor.

For example, using a lambda to implement complex sorting + traversal

#include <vector>#include <algorithm>#include <iostream>#include <string>using namespace std;// Custom structstruct Person{    string name;    int age;};int main(){    vector<Person> people = {        {"Alice", 25}, {"Bob", 20}, {"Charlie", 30}    };    // 1. Sort using lambda: by age ascending (if ages are the same, by name descending)    sort(people.begin(), people.end(),         [](const Person& p1, const Person& p2)         {            if (p1.age != p2.age)            {                return p1.age < p2.age;  // Age ascending            }            else            {                return p1.name > p2.name;  // Name descending            }        }    );    // 2. Traverse using lambda: print each person's information    for_each(people.begin(), people.end(),         [](const Person& p)         {            cout << "Name: " << p.name << ", Age: " << p.age << endl;        }    );    return 0;}// Output:// Name: Bob, Age: 20// Name: Alice, Age: 25// Name: Charlie, Age: 30

2. As function parameters: Implementing flexible callback logic

When a function requires “custom logic” as a parameter (such as filtering conditions, calculation rules), a lambda can be used as a callback function, replacing function pointers or functors.

In the following example: Implementing a “filter function” using a lambda to specify the filtering rules

#include <vector>#include <iostream>#include <functional>using namespace std;// Filter function: receives a vector and a lambda (callback function), returns elements that meet the conditionsvector<int> filter(const vector<int>& nums, function<bool(int)> condition){    vector<int> result;    for (int num : nums)  // Range for loop    {        if (condition(num))  // Call lambda to check if it meets the condition        {            result.push_back(num);        }    }    return result;}int main(){    vector<int> nums = {1,2,3,4,5,6,7,8,9};    // 1. Filter "even numbers" (lambda as callback)    auto evens = filter(nums, [](int num) { return num % 2 == 0; });    // 2. Filter "numbers greater than 5" (another lambda callback)    auto greater5 = filter(nums, [](int num) { return num > 5; });    // Print results    cout << "Even numbers:";    for (int n : evens)    {        cout << n << " ";    }    // Output: 2 4 6 8    cout << "\nNumbers greater than 5:";    for (int n : greater5)    {        cout << n << " ";    }    // Output: 6 7 8 9    return 0;}

3. As function return values: Encapsulating stateful logic

Lambda functions can be used as function return values (must be received with std::function), commonly used to encapsulate logic with “external state” (such as counters, configuration parameters).

In this example, returning an “accumulator” lambda that records the cumulative value

#include <iostream>#include <functional>using namespace std;// Return an accumulator lambda: initial value is init, adds step each timefunction<int()> create_accumulator(int init, int step){    // Capture init and step by value (ensure variables remain valid after return)    return [=]() mutable    {        init += step;        return init;    };}int main(){    // Create an accumulator: initial value 0, adds 2 each time    auto acc = create_accumulator(0, 2);    cout << acc() << endl;  // 0+2=2    cout << acc() << endl;  // 2+2=4    cout << acc() << endl;  // 4+2=6    // Create another accumulator: initial value 10, adds 5 each time    auto acc2 = create_accumulator(10, 5);    cout << acc2() << endl;  // 10+5=15    return 0;}

4. Capturing external variables: Flexibly reusing contextual data

The capture list of a lambda supports various capture methods to meet the needs of accessing external variables in different scenarios. Common capture methods are as follows:

Capture Method

Syntax

Description

Empty Capture

[]

Does not capture any external variables

Value Capture of a Single Variable

[x]

Captures a copy of x, cannot modify within the function body (must be mutable)

Reference Capture of a Single Variable

[&x]

Captures a reference to x, can modify external x within the function body

Value Capture of All Variables

[=]

Captures all external variables by value

Reference Capture of All Variables

[&]

Captures all external variables by reference

Mixed Capture

[=, &x]

Captures all variables by value, captures x by reference (x takes precedence)

Lambda functions can capture external variables, which refer to all visible local variables, function parameters, class member variables, etc., within their scope. Global variables and static variables do not need to be captured as their scope is outside the lambda function and can be used directly.

Example: Practical application of mixed capture

#include <iostream>#include <vector>#include <algorithm>using namespace std;int main(){    int threshold = 5;  // External threshold variable    vector<int> nums = {3,1,4,1,5,9,2,6};    vector<int> result;    // Mixed capture: captures threshold by value, captures result by reference    for_each(nums.begin(), nums.end(),         [=, &result](int num)        {            if (num > threshold)  // Use threshold captured by value            {                result.push_back(num);  // Store results in result captured by reference            }        }    );    cout << "Numbers greater than " << threshold << ": ";    for (int n : result)    {        cout << n << " ";    }    // Output: 6 9    return 0;}

Additionally, if a lambda function is defined within a class, it can capture class member variables through the this pointer. However, it is crucial to ensure that the object’s lifecycle covers the moment the lambda function is called, or it may lead to errors.

class MyClass {public:    auto getLambda() {        // Capture this pointer        return [this]() { return value; };    }private:    int value = 42;};// Dangerous: Calling lambda after object is destroyedMyClass* obj = new MyClass();auto lambda = obj->getLambda();delete obj;lambda(); // Undefined behavior!

5. Generic Lambda (C++14 Feature): Supports auto parameters

C++14 allows the parameter list of a lambda to use auto, achieving the effect of a “generic function” without explicitly declaring parameter types, adapting to various data types.

For example, implementing a generic addition using a generic lambda

#include <iostream>#include <string>using namespace std;int main(){    // Generic lambda: parameters a and b are of auto type (supporting int, double, string, etc.)    auto add = [](auto a, auto b)    {        return a + b;    };    cout << add(3, 5) << endl;          // Output: 8 (int+int)    cout << add(2.5, 4.7) << endl;      // Output: 7.2 (double+double)    cout << add("Hello ", "Lambda") << endl;  // Output: Hello Lambda (string+string)    return 0;}

Considerations When Using Lambda

While lambda functions are convenient, if details are overlooked, it is easy to introduce bugs. Here are some common pitfalls:

1. Reference capturing local variables: Beware of “dangling references”

If a lambda captures a “local variable” (such as a variable within a function) by reference, and the lambda’s lifecycle exceeds that of the local variable, it will lead to a “dangling reference” (accessing destroyed memory).

For example, in the following case:

#include <iostream>#include <functional>using namespace std;// Error: The returned lambda captures the local variable xfunction<int()> bad_lambda(){    int x = 10;  // Local variable, destroyed after function returns    return [&x]()    {        return x;    };  // Reference captures x, x no longer exists after return}int main(){    auto func = bad_lambda();    cout << func() << endl;  // Undefined behavior (may output random value)    return 0;}

Solution:

  • If you need to capture a local variable and the lambda’s lifecycle is long, usevalue capture (captures a copy, not dependent on the external variable’s lifecycle);
  • If you must capture by reference, ensure the external variable’s lifecycle covers the lambda’s lifecycle (such as using global variables or static variables, but be cautious with global variables).

2. Value capture’s “copy timing”: only copies once

Variables captured by value are only copied at the time of the lambda’sdefinition, subsequent modifications to the external variable will not affect the lambda’s copy.

#include <iostream>using namespace std;int main(){    int x = 10;    // At the time of defining the lambda, a copy of x is captured (at this point x=10)    auto lambda = [x]()    {        cout << x << endl;    };    x = 20;  // Subsequent modification of external x    lambda();  // Output: 10 (the lambda's copy is not updated)    return 0;}

As we can see, modifying the value of a variable captured by value does not affect the data inside the lambda.

Therefore, if you need to track the latest value of the captured variable, you can switch to reference capture.

However, capturing class member variables through the this pointer is an exception; in this case, the current value of the member variable is captured.

#include <iostream>using namespace std;class Test{    public:    void show()    {        // Capture member variable through this pointer        auto getlambda = [this]() {cout << value << endl;};        value = 20;  // Modify member variable        getlambda();    }    private:    int value = 10;};int main(){    Test t;    t.show();   // Output 20, the current value of the variable}

In the above code, the lambda function does not directly capture the value variable, but accesses it through this->value, so the output is the current value.

3. Using mutable: only modifies the copy, does not affect the external

By default, for external variables captured by value, the lambda function is not allowed to modify them. You can use the keywordmutable to break this restriction, but at this point, what is modified in the function is the “copy of the value captured”, and it will not actually modify the actual value of the external variable.

As shown below:

#include <iostream>using namespace std;int main(){    int x = 10;    // Add mutable, allows modification of the copy captured by value    auto lambda = [x]() mutable    {        x += 5; // If mutable is not used, this line is illegal        cout << "Lambda x inside:" << x << endl;     };    lambda();  // Output: 15 (the copy is modified)    cout << "External x:" << x << endl;  // Output: 10 (external remains unchanged)    return 0;}

If you need to synchronize the modification of the external variable, directly use

reference capture (no need for mutable);

4. Lambda cannot be recursive

Lambda functions cannot directly call themselves recursively because lambdas are anonymous and cannot reference themselves within the function body.

Error Example::

// Error: Lambda cannot reference itself (recurse is undefined)auto recurse = [](int n){    if (n == 0)    {        return 1;    }    return n * recurse(n-1);  // Compilation error: recurse not declared};

If you really want to use a lambda function recursively, you can wrap the lambda function with std::function and then reference that std::function object within the lambda.

#include <iostream>#include <functional>using namespace std;int main(){    // 1. First declare a std::function object    function<int(int)> factorial;    // 2. Define the lambda, reference capturing factorial    factorial = [&factorial](int n)    {        if (n == 0 || n == 1)        {            return 1;        }        return n * factorial(n-1);  // Recursive call to factorial    };    cout << "Factorial of 5:" << factorial(5) << endl;  // Output: 120 (correct)    return 0;}

Choosing Capture Method: Value Capture vs Reference Capture

  • Value Capture will copy external variables; if the variable is large (such as a large struct or container), the copy overhead is high;
  • Reference Capture has no copy overhead, but care must be taken regarding lifecycle issues (to avoid dangling references).

The principle of choosing between value capture and reference capture is similar to choosing between value passing and reference passing for function parameters, mainly depending on the type of variable to be captured and whether it needs to track the variable:

  • If capturing small variables (int, double, pointers, etc.): Prefer value capture (safe, low overhead); at the same time, capturing is a copy of the external variable, unaffected by changes to the external variable.
  • If capturing large variables (vector, string, custom structs): Prefer reference capture (to reduce copy overhead), but ensure the variable’s lifecycle covers the lambda; at the same time, you can always get the current value of the external variable.

Conclusion: The Core Advantages of Lambda Functions

  • Code Conciseness Inline definition of temporary function logic reduces template code by over 80%
  • Logical Cohesion Tightly integrates function definitions with usage scenarios, enhancing readability
  • Powerful Functionality Supports capturing context, generic programming, compile-time calculations, and other advanced features
  • Seamless Integration Perfectly collaborates with modern C++ components such as STL algorithms and multithreading libraries

Lambda functions are one of the core features of C++11 and later. Mastering them can make your code more concise and flexible.

Feel free to follow for continuous sharing of insights and experiences in learning C++.

Leave a Comment