Introduction
Lambda expressions, introduced in C++11, have revolutionized the way we write and use functions in C++. They provide a concise and convenient way to create anonymous function objects at the point where they are needed. This tutorial will delve into the details of working with lambda expressions in C++, targeting non-beginner programmers who already have a solid understanding of the language but want to deepen their knowledge of this powerful feature.
What Are Lambda Expressions?
A lambda expression in C++ is essentially a way to define an anonymous function, i.e., a function without a name. Lambda expressions can be used wherever function objects are required, such as in algorithms, callbacks, or in custom function templates. They allow you to write inline functions in a compact form, enhancing code readability and maintainability.
Syntax of Lambda Expressions
The general syntax of a lambda expression is:
[capture](parameters) -> return_type { body }
Code language: C++ (cpp)
capture
: Specifies which variables from the surrounding scope are to be captured and made available inside the lambda.parameters
: Specifies the parameters passed to the lambda, similar to regular functions.return_type
: Specifies the return type of the lambda. This is optional if the return type can be inferred.body
: The code that defines the behavior of the lambda.
Basic Example
Let’s start with a simple example to illustrate the basic usage of a lambda expression:
#include <iostream>
int main() {
auto add = [](int a, int b) -> int {
return a + b;
};
int result = add(5, 3);
std::cout << "Result: " << result << std::endl; // Output: Result: 8
return 0;
}
Code language: C++ (cpp)
In this example, we define a lambda expression add
that takes two integers and returns their sum. We then call this lambda with the arguments 5 and 3 and print the result.
Capturing Variables
One of the key features of lambda expressions is the ability to capture variables from their enclosing scope. There are several ways to capture variables:
Capture by Value
When you capture a variable by value, the lambda makes a copy of the variable. Changes to the variable inside the lambda do not affect the original variable outside the lambda.
#include <iostream>
int main() {
int x = 10;
auto printX = [x]() {
std::cout << "x: " << x << std::endl;
};
x = 20;
printX(); // Output: x: 10
return 0;
}
Code language: C++ (cpp)
Capture by Reference
When you capture a variable by reference, the lambda has access to the original variable. Changes to the variable inside the lambda affect the original variable.
#include <iostream>
int main() {
int x = 10;
auto printX = [&x]() {
std::cout << "x: " << x << std::endl;
};
x = 20;
printX(); // Output: x: 20
return 0;
}
Code language: C++ (cpp)
Capture by Value and Reference
You can capture multiple variables, some by value and some by reference.
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto printXY = [x, &y]() {
std::cout << "x: " << x << ", y: " << y << std::endl;
};
x = 30;
y = 40;
printXY(); // Output: x: 10, y: 40
return 0;
}
Code language: C++ (cpp)
Capture All Variables
You can also capture all variables from the enclosing scope, either by value or by reference.
- Capture all by value:
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto printXY = [=]() {
std::cout << "x: " << x << ", y: " << y << std::endl;
};
x = 30;
y = 40;
printXY(); // Output: x: 10, y: 20
return 0;
}
Code language: C++ (cpp)
- Capture all by reference:
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto printXY = [&]() {
std::cout << "x: " << x << ", y: " << y << std::endl;
};
x = 30;
y = 40;
printXY(); // Output: x: 30, y: 40
return 0;
}
Code language: C++ (cpp)
Mixed Capture Modes
You can mix capturing all variables by reference while capturing specific variables by value.
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto printXY = [=, &y]() {
std::cout << "x: " << x << ", y: " << y << std::endl;
};
x = 30;
y = 40;
printXY(); // Output: x: 10, y: 40
return 0;
}
Code language: C++ (cpp)
Lambda Expressions with STL Algorithms
One of the most powerful uses of lambda expressions is in conjunction with the Standard Template Library (STL) algorithms. Lambda expressions can be used as predicates or function objects in algorithms like std::sort
, std::for_each
, std::find_if
, etc.
Example: Using Lambda with std::sort
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 3};
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b;
});
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl; // Output: 1 2 3 5 8
return 0;
}
Code language: C++ (cpp)
Example: Using Lambda with std::for_each
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 3};
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl; // Output: 5 2 8 1 3
return 0;
}
Code language: C++ (cpp)
Example: Using Lambda with std::find_if
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 3};
auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) {
return n > 3;
});
if (it != numbers.end()) {
std::cout << "Found a number greater than 3: " << *it << std::endl; // Output: Found a number greater than 3: 5
} else {
std::cout << "No number greater than 3 found" << std::endl;
}
return 0;
}
Code language: C++ (cpp)
Generic Lambdas
Starting from C++14, lambda expressions can be generic, which means they can accept parameters of any type without explicitly specifying the types. This is achieved using the auto
keyword.
Example: Generic Lambda
#include <iostream>
int main() {
auto print = [](auto value) {
std::cout << value << std::endl;
};
print(10); // Output: 10
print(3.14); // Output: 3.14
print("Hello"); // Output: Hello
return 0;
}
Code language: C++ (cpp)
Example: Generic Lambda with Multiple Parameters
#include <iostream>
int main() {
auto add = [](auto a, auto b) {
return a + b;
};
std::cout << add(5, 3) << std::endl; // Output: 8
std::cout << add(2.5, 1.5) << std::endl; // Output: 4
std::cout << add(std::string("Hello "), std::string("World")) << std::endl; // Output: Hello World
return 0;
}
Code language: C++ (cpp)
Stateful Lambdas
Lambdas can also maintain state across multiple calls by using the mutable
keyword. By default, captured variables are treated as const
within the lambda body. To modify the captured variables, the lambda must be marked as mutable
.
Example: Mutable Lambda
#include <iostream>
int main() {
int counter = 0;
auto increment = [counter]() mutable {
return ++counter;
};
std::cout << increment() << std::endl; // Output: 1
std::cout << increment() << std::endl; // Output: 2
std::cout << increment() << std::endl; // Output: 3
return 0;
}
Code language: C++ (cpp)
In this example, the counter
variable is captured by value, but since the lambda is marked as mutable
, it can modify the captured variable.
Lambda Expressions in Classes and Structs
Lambdas can be used inside classes and structs, allowing you to define small helper functions in a concise way.
Example: Lambda in a Class
#include <iostream>
#include <vector>
#include <algorithm>
class NumberCollection {
public:
void addNumber(int number) {
numbers.push_back(number);
}
void printNumbers() const {
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl;
}
private:
std::vector<int> numbers;
};
int main() {
NumberCollection collection;
collection.addNumber(1);
collection.addNumber(2);
collection.addNumber(3);
collection.printNumbers(); // Output: 1 2 3
return 0;
}
Code language: C++ (cpp)
Capturing this Pointer
When a lambda is defined inside a member function, you can capture the this
pointer to access members of the class.
Example: Capturing this Pointer
#include <iostream>
#include <vector>
#include <algorithm>
class NumberCollection {
public:
void addNumber(int number) {
numbers.push_back(number);
}
void printNumbers() const {
std::for_each(numbers.begin(), numbers.end(), [this](int n) {
std::cout << n * multiplier << " ";
});
std::cout << std::endl;
}
private:
std::vector<int> numbers;
int multiplier = 2;
};
int main() {
NumberCollection collection;
collection.addNumber(1);
collection.addNumber(2);
collection.addNumber(3);
collection.printNumbers(); // Output: 2 4 6
return 0;
}
Code language: C++ (cpp)
In this example, the lambda captures the this
pointer to access the multiplier
member of the NumberCollection
class.
Advanced Lambda Features
Recursive Lambdas
Lambdas can also be recursive, although this requires some workarounds because lambdas do not have a name to refer to themselves. One common approach is to use a std::function
to enable recursion.
Example: Recursive Lambda
#include <iostream>
#include <functional>
int main() {
std::function<int(int)> factorial = [&](int n) {
if (n <= 1) return 1;
else return n * factorial(n - 1);
};
std::cout << "Factorial of 5: " << factorial(5) << std::endl; // Output: Factorial of 5: 120
return 0;
}
Code language: C++ (cpp)
Lambdas with Default Arguments
Lambdas can have default arguments just like regular functions.
Example: Lambda with Default Arguments
#include <iostream>
int main() {
auto greet = [](std::string name = "World") {
std::cout << "Hello, " << name << "!" << std::endl;
};
greet(); // Output: Hello, World!
greet("Alice"); // Output: Hello, Alice!
return 0;
}
Code language: C++ (cpp)
Lambdas as Template Parameters
Lambdas can be passed as template parameters, allowing you to write highly flexible and reusable code.
Example: Lambda as Template Parameter
#include <iostream>
#include <vector>
#include <algorithm>
template <typename Func>
void applyToVector(std::vector<int>& vec, Func func) {
std::for_each(vec.begin(), vec.end(), func);
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
applyToVector(numbers, [](int& n) {
n *= 2;
});
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl; // Output: 2 4 6 8 10
return 0;
}
Code language: C++ (cpp)
In this example, the applyToVector
function template takes a lambda as a parameter and applies it to each element in the vector.
Practical Use Cases for Lambda Expressions
Sorting Custom Objects
Lambdas are particularly useful when sorting collections of custom objects.
Example: Sorting Custom Objects
#include <iostream>
#include <vector>
#include <algorithm>
class Person {
public:
Person(std::string name, int age) : name(name), age(age) {}
std::string getName() const { return name; }
int getAge() const { return age; }
private:
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
Person("Alice", 30),
Person("Bob", 25),
Person("Charlie", 35)
};
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
return a.getAge() < b.getAge();
});
for (const Person& person : people) {
std::cout << person.getName() << ": " << person.getAge() << std::endl;
}
// Output:
// Bob: 25
// Alice: 30
// Charlie: 35
return 0;
}
Code language: C++ (cpp)
Event Handling and Callbacks
Lambdas are also commonly used for event handling and callbacks, especially in GUI programming.
Example: Using Lambda for Callbacks
#include <iostream>
#include <functional>
void registerCallback(std::function<void(int)> callback) {
callback(42);
}
int main() {
registerCallback([](int value) {
std::cout << "Callback received value: " << value << std::endl;
});
return 0;
}
Code language: C++ (cpp)
Functional Programming
Lambda expressions facilitate functional programming techniques in C++, such as higher-order functions and composition.
Example: Higher-Order Functions
#include <iostream>
#include <vector>
#include <algorithm>
std::function<int(int)> makeMultiplier(int factor) {
return [factor](int value) {
return value * factor;
};
}
int main() {
auto doubleValue = makeMultiplier(2);
auto tripleValue = makeMultiplier(3);
std::cout << "Double of 5: " << doubleValue(5) << std::endl; // Output: Double of 5: 10
std::cout << "Triple of 5: " << tripleValue(5) << std::endl; // Output: Triple of 5: 15
return 0;
}
Code language: C++ (cpp)
Example: Function Composition
#include <iostream>
#include <functional>
auto compose(std::function<int(int)> f, std::function<int(int)> g) {
return [f, g](int x) {
return f(g(x));
};
}
int main() {
auto addOne = [](int x) {
return x + 1;
};
auto doubleValue = [](int x) {
return x * 2;
};
auto addOneThenDouble = compose(doubleValue, addOne);
std::cout << "Result: " << addOneThenDouble(5) << std::endl; // Output: Result: 12
return 0;
}
Code language: C++ (cpp)
Conclusion
Lambda expressions are a powerful feature in C++ that enhance the language’s expressiveness and flexibility. They provide a concise way to define anonymous functions and can be used in various scenarios, such as STL algorithms, event handling, and functional programming. By understanding how to work with lambda expressions, you can write more readable, maintainable, and efficient C++ code.