Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Creating Inline Functions
Define inline function objects without declaring a separate named function.
Introduction to Lambdas (Anonymous Functions)
Consider this code from an earlier lesson on standard library algorithms:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
bool containsVowel(std::string_view str)
{
return (str.find('a') != std::string_view::npos ||
str.find('e') != std::string_view::npos ||
str.find('i') != std::string_view::npos ||
str.find('o') != std::string_view::npos ||
str.find('u') != std::string_view::npos);
}
int main()
{
constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};
auto result{std::find_if(words.begin(), words.end(), containsVowel)};
if (result == words.end())
{
std::cout << "No words with vowels\n";
}
else
{
std::cout << "Found: " << *result << '\n';
}
return 0;
}
Output:
Found: blue
This code works, but has some issues. Because std::find_if requires a function pointer, we must:
- Define a named function (
containsVowel) - Place it at global scope (functions can't be nested in C++)
- Use it only once, yet it clutters the global namespace
The function is so short that its purpose is almost clearer from reading the code than from its name and comments.
Lambdas Solve This Problem
A lambda expression (often called a lambda or closure) lets us define an anonymous function inside another function. This allows us to:
- Keep the function definition close to where it's used
- Avoid polluting the global namespace
- Avoid naming functions used only once
Lambda syntax:
[ captureClause ] ( parameters ) -> returnType
{
statements;
}
Components:
- capture clause: Can be empty
[]if no captures needed - parameters: Can be empty or omitted entirely (unless specifying a return type)
- return type: Optional; if omitted,
autois assumed and the compiler deduces the type - body: The function implementation
Lambdas have no name (they're anonymous), so we don't provide one.
A trivial lambda:
#include <iostream>
int main()
{
[]{}; // Lambda with no captures, no parameters, omitted return type, empty body
return 0;
}
Let's rewrite our example using a lambda:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};
// Define the function right where we use it
auto result{std::find_if(words.begin(), words.end(),
[](std::string_view str) // Lambda with empty capture clause
{
return (str.find('a') != std::string_view::npos ||
str.find('e') != std::string_view::npos ||
str.find('i') != std::string_view::npos ||
str.find('o') != std::string_view::npos ||
str.find('u') != std::string_view::npos);
})};
if (result == words.end())
{
std::cout << "No words with vowels\n";
}
else
{
std::cout << "Found: " << *result << '\n';
}
return 0;
}
Output:
Found: blue
The lambda and the containsVowel function are very similar:
- Same parameter type (
std::string_view) - Same function body
- The lambda has an empty capture clause
[](explained in the next lesson) - We omitted the trailing return type in the lambda; since we return a
bool, the compiler deducesbool
When you need a trivial, one-off function to pass as an argument, prefer lambdas over global functions. This follows the principle of defining things in the smallest scope and closest to first use.
Lambda Types
In the example above, we defined a lambda right where it's used. This is called a function literal.
Sometimes writing a lambda inline makes code harder to read. Just as we can initialize variables with values for later use, we can initialize lambda variables with lambda definitions.
Here's a lambda used inline:
// Hard to read at a glance
return std::all_of(numbers.begin(), numbers.end(), [](int n){ return ((n % 2) == 0); });
Better - store in a named variable:
// Easier to understand
auto isEven{
[](int n)
{
return (n % 2) == 0;
}
};
return std::all_of(numbers.begin(), numbers.end(), isEven);
The last line reads naturally: "return whether all of the elements in numbers are even."
Storing a lambda in a variable provides a useful name, making code more readable and allowing reuse of the lambda.
What Type is a Lambda?
When you write a lambda, the compiler generates a unique type for it that isn't exposed to you.
Advanced note: Lambdas aren't actually functions (which is how C++ avoids the restriction on nested functions). They're special objects called functors - objects with an overloaded operator() that makes them callable like functions.
Since we don't know a lambda's exact type, we have several ways to store it:
#include <functional>
int main()
{
// Method 1: Function pointer (only works with empty capture clause [])
double (*multiply1)(double, double){
[](double x, double y) {
return x * y;
}
};
multiply1(3.0, 4.0);
// Method 2: std::function (works with any capture clause)
std::function multiply2{ // Pre-C++17: std::function<double(double, double)>
[](double x, double y) {
return x * y;
}
};
multiply2(5.0, 6.0);
// Method 3: auto (stores with real type, most efficient)
auto multiply3{
[](double x, double y) {
return x * y;
}
};
multiply3(7.0, 8.0);
return 0;
}
auto is the only method that uses the lambda's actual type, making it the most efficient (no overhead).
Passing Lambdas to Functions
There are 4 ways to pass a lambda to a function:
#include <functional>
#include <iostream>
// Option 1: std::function parameter
void execute1(int count, const std::function<void(int)>& fn)
{
for (int i{0}; i < count; ++i)
fn(i);
}
// Option 2: Template with type parameter
template <typename T>
void execute2(int count, const T& fn)
{
for (int i{0}; i < count; ++i)
fn(i);
}
// Option 3: Abbreviated template (C++20)
void execute3(int count, const auto& fn)
{
for (int i{0}; i < count; ++i)
fn(i);
}
// Option 4: Function pointer (only for lambdas with no captures)
void execute4(int count, void (*fn)(int))
{
for (int i{0}; i < count; ++i)
fn(i);
}
int main()
{
auto printValue{[](int i) { std::cout << i << '\n'; }};
execute1(3, printValue);
execute2(3, printValue);
execute3(3, printValue);
execute4(3, printValue);
return 0;
}
- Option 1 (
std::function): Shows explicit parameter and return types, but requires conversion overhead. Can separate declaration and definition. - Option 2 (template): More efficient, no conversion overhead, but parameter/return types not obvious
- Option 3 (C++20
auto): Generates same code as option 2, cleaner syntax - Option 4 (function pointer): Only works for lambdas with empty capture clauses
When storing a lambda in a variable, use `auto`.
When passing a lambda to a function:
- If C++20 capable, use `auto` as the parameter type
- Otherwise, use a template with a type parameter or `std::function` (or function pointer if the lambda has no captures)
Generic Lambdas
Lambda parameters follow the same rules as regular function parameters, with one notable exception: since C++14, you can use auto for lambda parameters. When a lambda has one or more auto parameters, the compiler infers the types from calls to the lambda.
Lambdas with auto parameters are called generic lambdas because they can work with multiple types.
Advanced note: In lambdas, auto parameters are shorthand for template parameters.
Example:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array days{
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
// Find two consecutive days starting with the same letter
const auto match{std::adjacent_find(days.begin(), days.end(),
[](const auto& a, const auto& b) {
return a[0] == b[0];
})};
if (match != days.end())
{
std::cout << *match << " and " << *std::next(match)
<< " start with the same letter\n";
}
return 0;
}
Output:
Saturday and Sunday start with the same letter
Using auto for parameters lets our lambda work with any string type that supports operator[]. If we change days to use std::string later, the lambda still works.
However, auto isn't always best:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array days{
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
// Count days with exactly 6 letters
const auto sixLetterDays{std::count_if(days.begin(), days.end(),
[](std::string_view str) {
return str.length() == 6;
})};
std::cout << "Days with 6 letters: " << sixLetterDays << '\n';
return 0;
}
Output:
Days with 6 letters: 2
Here, auto would infer const char*, which is awkward to work with. By explicitly specifying std::string_view, we get a much more convenient interface with methods like length().
Constexpr Lambdas
As of C++17, lambdas are implicitly constexpr if they satisfy constant expression requirements:
- All captures must be
constexpr(or no captures) - All functions called must be
constexpr
In C++20, many standard library functions became constexpr, making more lambdas eligible:
constexpr auto sixLetterDays{std::count_if(days.begin(), days.end(),
[](std::string_view str) {
return str.length() == 6;
})};
Generic Lambdas and Static Variables
Generic lambdas with auto parameters generate a unique lambda for each type encountered, similar to function templates.
This affects static variables:
#include <iostream>
int main()
{
auto display{
[](auto value) {
static int counter{0};
std::cout << counter++ << ": " << value << '\n';
}
};
display("hello"); // 0: hello
display("world"); // 1: world
display(100); // 0: 100
display(200); // 1: 200
display("test"); // 2: test
return 0;
}
Output:
0: hello
1: world
0: 100
1: 200
2: test
Two lambda versions were generated (one for const char*, one for int), each with its own counter. String calls share one counter, integer calls share a different counter.
Return Type Deduction
If using return type deduction, all return statements must return the same type:
// ERROR: inconsistent return types
auto calculate{[](int x, int y, bool useFloat) {
if (useFloat)
return static_cast<double>(x) / y; // returns double
else
return x / y; // returns int - ERROR!
}};
Solutions:
- Cast all returns to match
- Explicitly specify a return type (usually better):
// Better: explicit return type allows implicit conversions
auto calculate{[](int x, int y, bool useFloat) -> double {
if (useFloat)
return static_cast<double>(x) / y;
else
return x / y; // implicitly converts to double
}};
Standard Library Function Objects
For common operations, the standard library provides callable objects in <functional>:
#include <algorithm>
#include <array>
#include <iostream>
#include <functional>
int main()
{
std::array numbers{25, 15, 30, 10, 40, 20};
// Using std::greater instead of writing a lambda
std::sort(numbers.begin(), numbers.end(), std::greater{});
for (int n : numbers)
std::cout << n << ' ';
std::cout << '\n';
return 0;
}
Output:
40 30 25 20 15 10
Conclusion
Lambdas combined with the algorithm library enable powerful operations in just a few lines of readable code. They also support parallelism easily, which loops don't provide.
However, lambdas don't replace regular functions in all cases. Use regular functions for non-trivial, reusable logic.
Quiz
Question 1: Create a struct Employee storing name and salary. Find and print the employee with the highest salary using std::max_element.
Given:
std::array<Employee, 7> employees{
{{"Alice", 45000},
{"Bob", 52000},
{"Carol", 38000},
{"David", 61000}, // highest
{"Eve", 48000},
{"Frank", 43000},
{"Grace", 55000}}
};
Expected output: David has the highest salary
Show Solution
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct Employee
{
std::string_view name{};
int salary{};
};
int main()
{
constexpr std::array<Employee, 7> employees{
{{"Alice", 45000},
{"Bob", 52000},
{"Carol", 38000},
{"David", 61000},
{"Eve", 48000},
{"Frank", 43000},
{"Grace", 55000}}
};
const auto highest{
std::max_element(employees.begin(), employees.end(),
[](const auto& a, const auto& b) {
return a.salary < b.salary;
})
};
std::cout << highest->name << " has the highest salary\n";
return 0;
}
Question 2: Sort the following cities by population (ascending) using std::sort and a lambda:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct City
{
std::string_view name{};
int population{};
};
int main()
{
std::array<City, 4> cities{
{{"Tokyo", 37400000},
{"Delhi", 28514000},
{"Shanghai", 25582000},
{"Sao Paulo", 21650000}}
};
// Sort here
for (const auto& city : cities)
std::cout << city.name << '\n';
return 0;
}
Expected output:
Sao Paulo
Shanghai
Delhi
Tokyo
Show Solution
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct City
{
std::string_view name{};
int population{};
};
int main()
{
std::array<City, 4> cities{
{{"Tokyo", 37400000},
{"Delhi", 28514000},
{"Shanghai", 25582000},
{"Sao Paulo", 21650000}}
};
std::sort(cities.begin(), cities.end(),
[](const auto& a, const auto& b) {
return a.population < b.population;
});
for (const auto& city : cities)
std::cout << city.name << '\n';
return 0;
}
Summary
Lambda expressions: Anonymous functions that can be defined inline within another function, eliminating the need to create named global functions for one-off operations.
Lambda syntax: [ captureClause ] ( parameters ) -> returnType { statements; } where the capture clause, parameters, and return type are all optional depending on usage.
Function literals: Lambdas defined directly where they're used, particularly useful when passing functions as arguments to algorithms.
Lambda types: Each lambda has a unique compiler-generated type. Lambdas can be stored using function pointers (empty captures only), std::function (any lambda, with overhead), or auto (most efficient, preserves actual type).
Generic lambdas: Lambdas with auto parameters that work with multiple types, effectively creating template-like behavior. Since C++14, auto parameters allow type deduction from usage.
Constexpr lambdas: As of C++17, lambdas are implicitly constexpr if they satisfy constant expression requirements (constexpr captures and calls).
Static variables in generic lambdas: Each instantiation of a generic lambda gets its own set of static variables, similar to function templates.
Return type deduction: When using return type deduction, all return statements must return the same type. Specify an explicit return type to allow implicit conversions between return types.
Lambdas combined with standard library algorithms enable powerful, readable operations. They're ideal for trivial, one-off operations but shouldn't replace regular functions for complex, reusable logic. The standard library provides common function objects in <functional> (like std::greater) for typical operations, eliminating the need to write custom lambdas for simple comparisons.
Creating Inline Functions - Quiz
Test your understanding of the lesson.
Practice Exercises
Introduction to Lambdas
Practice creating and using lambda expressions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!