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.
#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 thethis
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 thereturn
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
: Likenoexcept
.
3. Lambdas with STL Algorithms (<algorithm>
)
Lambdas shine when used with STL algorithms that expect predicates (return bool) or functions to apply.
#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.
#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>
) likefind_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:
- cppreference – Lambda Expressions: https://en.cppreference.com/w/cpp/language/lambda
- cppreference – Standard Algorithms: https://en.cppreference.com/w/cpp/algorithm
- Learn C++ – Lambdas: https://www.learncpp.com/cpp-tutorial/introduction-to-lambdas-anonymous-functions/