C++ for C Developers | Lesson 14: Introduction to Lambdas & Basic STL Algorithms

Goal: Learn the syntax and usage of C++11 lambda expressions (anonymous functions). See how they provide a concise way to define operations inline, especially when used with common STL algorithms like find_if, sort, for_each, and count_if.

1. The Problem: Passing Simple Operations to Functions

Often, when using STL algorithms or other functions that accept function pointers or function objects (functors), the operation you want to perform is simple and specific to that one call site. Writing a separate named function or a whole functor class just for that small operation can be verbose and separates the logic from where it’s used.

C++
#include <vector>
#include <algorithm> // For std::find_if

// We need a function to check if a number is even for std::find_if
bool isEven(int n) {
    return n % 2 == 0;
}

int main_old_style() {
    std::vector<int> nums = {1, 3, 5, 4, 7, 8};
    // Find the first even number using a separate function
    auto it = std::find_if(nums.begin(), nums.end(), isEven);
    // ...
    return 0;
}

This isn’t too bad for isEven, but if the condition was more complex or specific to this one search, defining a separate function feels heavy.

2. Lambda Expressions: Anonymous Functions Inline

C++11 introduced lambda expressions to define anonymous function objects directly where they are needed.

flowchart LR
    Start([Lambda Syntax])

    subgraph "Core Syntax"
        C1["[captures](parameters) -> return_type { body }"]
    end

    subgraph "Capture Clause []"
        CC0["[] — captures nothing"]
        CC1["[=] — capture by copy"]
        CC2["[&] — capture by reference"]
        CC3["[var1, &var2] — specific capture"]
        CC4["[this] — capture class members"]
    end

    subgraph "Parameters & Body"
        P1["(int x) or empty ()"]
        P2["{ logic }"]
    end

    subgraph "Return Type & Modifiers"
        R1["-> type (optional, deduced if omitted)"]
        R2["mutable — modify captured-by-copy"]
        R3["noexcept — optional exception specifier"]
    end

    Start --> C1
    C1 --> CC0
    C1 --> CC1
    C1 --> CC2
    C1 --> CC3
    C1 --> CC4
    C1 --> P1
    C1 --> P2
    C1 --> R1
    C1 --> R2
    C1 --> R3
  • Basic Syntax: [capture_list](parameter_list) mutable_specifier exception_specifier -> return_type { function_body }

Let’s break down the essential parts:

  • [] (Capture Clause):Crucial part. Specifies which variables from the surrounding scope the lambda can access and how.
    • []: Captures nothing. The lambda can only use its parameters and local variables.
    • [=]: Captures all used variables from the surrounding scope by copy (value). The lambda gets copies of the variables’ values at the time the lambda is created.
    • [&]: Captures all used variables from the surrounding scope by reference. The lambda can access (and potentially modify) the original variables. Caution: Ensure the lambda doesn’t outlive the referenced variables!
    • [var1, &var2]: Captures specific variables (e.g., var1 by copy, var2 by reference).
    • [this]: (Inside class member functions) Captures the this pointer, allowing access to member variables/functions.
  • () (Parameter List): Just like a regular function’s parameters (e.g., (int x, const std::string& s)). Can be omitted if the lambda takes no parameters: [] { ... }.
  • {} (Function Body): The code the lambda executes.
  • -> return_type (Optional Trailing Return Type): Usually, the compiler can deduce the return type from the return statements in the body. You only need to specify it explicitly if deduction fails or if you want to be precise (e.g., -> bool).
  • mutable: Allows modifying variables captured by copy ([=]). Rarely needed.
  • exception_specifier: Like noexcept.

3. Lambdas with STL Algorithms (<algorithm>)

Lambdas shine when used with STL algorithms that expect predicates (return bool) or functions to apply.

C++
#include <iostream>
#include <vector>
#include <string>
#include <algorithm> // Essential for algorithms!
#include <functional> // Often included indirectly, sometimes needed explicitly

int main() {
    std::vector<int> numbers = {1, 5, 2, 8, 3, 7, 4, 6};
    int threshold = 4;
    std::vector<std::string> words = {"C++", "is", "powerful", "and", "fun"};

    // --- std::find_if ---
    // Find the first element greater than 'threshold'
    // [threshold] captures 'threshold' by copy. (int n) is the parameter passed by find_if.
    auto it_greater = std::find_if(numbers.begin(), numbers.end(),
                                   [threshold](int n) -> bool {
                                       return n > threshold;
                                   });

    if (it_greater != numbers.end()) {
        std::cout << "First element > " << threshold << ": " << *it_greater << std::endl; // Output: 5
    }

    // --- std::count_if ---
    // Count how many elements are even
    // [] captures nothing. Lambda only uses its parameter 'n'.
    size_t even_count = std::count_if(numbers.begin(), numbers.end(),
                                      [](int n) { return n % 2 == 0; }); // -> bool deduced
    std::cout << "Number of even elements: " << even_count << std::endl; // Output: 4

    // --- std::sort (with custom comparator) ---
    // Sort words by length (shortest first)
    // Takes two arguments, returns true if first should come before second
    std::sort(words.begin(), words.end(),
              [](const std::string& a, const std::string& b) {
                  return a.length() < b.length();
              });

    std::cout << "Words sorted by length:";
    for (const auto& w : words) std::cout << " " << w;
    std::cout << std::endl; // Output: is and fun C++ powerful

    // --- std::for_each ---
    // Apply an operation to each element (e.g., print with modification)
    // [&] captures by reference (though not needed here, just example)
    std::cout << "Printing numbers doubled:";
    std::for_each(numbers.begin(), numbers.end(),
                  [&](int n) {
                      std::cout << " " << (n * 2);
                  });
    std::cout << std::endl;

    // --- Capturing by Reference for Modification ---
    int sum = 0;
    std::cout << "Calculating sum (using reference capture):";
    // [&sum] captures 'sum' by reference, allowing the lambda to modify the original 'sum'
    std::for_each(numbers.begin(), numbers.end(),
                  [&sum](int n) {
                      sum += n;
                  });
    std::cout << " Sum = " << sum << std::endl; // Output: 36


    // --- Assigning lambda to a variable ---
    // Type is deduced by auto (it's some compiler-generated unnamed type)
    auto isOdd = [](int n) { return n % 2 != 0; };
    size_t odd_count = std::count_if(numbers.begin(), numbers.end(), isOdd);
    std::cout << "Number of odd elements: " << odd_count << std::endl; // Output: 4


    return 0;
}

flowchart TD
    subgraph "STL + Lambdas"
        A1["std::find_if"]
        A2["std::count_if"]
        A3["std::sort"]
        A4["std::for_each"]
    end

    subgraph "Lambdas Used"
        L1["[threshold](int n) { return n > threshold; }"]
        L2["[](int n) { return n % 2 == 0; }"]
        L3["[](const std::string& a, const std::string& b) { return a.length() < b.length(); }"]
        L4["[&](int n) { std::cout << n * 2; }"]
    end

    A1 --> L1
    A2 --> L2
    A3 --> L3
    A4 --> L4

4. Understanding Captures

flowchart LR
    C1["[=] Capture by Copy"]
    C2["Lambda gets a *copy* of variable"]
    C3["Changes to original do NOT affect lambda"]

    R1["[&] Capture by Reference"]
    R2["Lambda holds *reference* to original"]
    R3["Changes to original ARE seen by lambda"]

    C1 --> C2 --> C3
    R1 --> R2 --> R3
  • [=] (Capture by Copy): The lambda gets a copy of the variable’s value at the point the lambda is created. Changes to the original variable after the lambda is created won’t affect the lambda’s copy.
  • [&] (Capture by Reference): The lambda holds a reference to the original variable. Changes to the original variable will be reflected when the lambda uses it. Danger: If the lambda is stored and called after the original variable goes out of scope, you’ll have a dangling reference (undefined behavior!). Be careful with lifetimes when capturing by reference.
C++
#include <iostream>
#include <functional> // For std::function

std::function<void()> createPrinter(int val) {
    // [=] captures 'val' by copy when createPrinter is called
    return [=]() {
        std::cout << "Value captured by copy: " << val << std::endl;
    };
}

std::function<void()> createRefPrinter(int& val_ref) {
     // [&] captures 'val_ref' by reference when createRefPrinter is called
    return [&]() {
        std::cout << "Value captured by reference: " << val_ref << std::endl;
    };
}

int main() {
    int x = 10;
    auto printer1 = createPrinter(x); // Captures x=10 by copy

    int y = 20;
    auto printer2 = createRefPrinter(y); // Captures reference to y

    x = 11; // Change original x
    y = 22; // Change original y

    std::cout << "Calling printers after modification:\n";
    printer1(); // Prints 10 (captured the copy before x changed)
    printer2(); // Prints 22 (sees the change via reference)

    // Dangling reference example (DON'T DO THIS)
    std::function<void()> dangling_printer;
    {
       int z = 30;
       dangling_printer = createRefPrinter(z); // Captures reference to z
    } // z goes out of scope HERE!
    // dangling_printer(); // CALLING THIS IS UNDEFINED BEHAVIOR! The reference dangles.

    return 0;
}

Summary of Lesson 14:

  • Lambda expressions provide a concise syntax [captures](params){body} to create anonymous function objects inline.
  • The capture clause ([]) controls how lambdas access variables from the surrounding scope (by copy [=], by reference [&], or specifically).
  • Lambdas are extremely useful with STL algorithms (<algorithm>) like find_if, count_if, sort, for_each, allowing you to specify predicates or operations directly where they are used.
  • Be mindful of variable lifetimes when capturing by reference ([&]) to avoid dangling references. Capture by copy ([=]) is often safer if you don’t need to modify the original or see later changes.

Next Lesson: We’ll cover the different C++ casting operators (static_cast, dynamic_cast, etc.) and contrast them with C-style casts, followed by a final series wrap-up and pointers for further learning.

More sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top