Functional programming in c++
Core Concepts of Functional Programming
- Pure Functions: A pure function always produces the same output for the same input and has no side effects (doesn’t modify any external state). This makes code more predictable and easier to test.
- Immutability: Immutability means that data cannot be changed after it’s created. This eliminates bugs caused by unintended modifications and makes concurrency easier.
- First-class Functions: Functions can be treated as values — passed as arguments, returned from functions, and stored in variables.
- Higher-Order Functions: Functions that take other functions as arguments or return functions as results.
Lambda Expressions
Anonymous functions that you can define inline.
Basic Syntax:
[capture list](parameters) -> return type { function body }
- Capture list: Specifies which variables from the surrounding scope the lambda can access.
- Parameters: Similar to regular function parameters.
- Return type: The type of value the lambda returns (can often be omitted, as the compiler can deduce it).
- Function body: The code that the lambda executes.
Example #1
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Lambda expression to square a number
auto square = [](int n) { return n * n; };
// Use the lambda with std::transform
std::vector<int> squares(numbers.size());
std::transform(numbers.begin(), numbers.end(), squares.begin(), square);
for (int n : squares) {
std::cout << n << " "; // Output: 1 4 9 16 25
}
std::cout << std::endl;
return 0;
}
Example #2
#include <algorithm>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int n){ std::cout << n * 2 << " "; });
// Output: 2 4 6 8 10
return 0;
}
Function Objects (Functors):
Objects that can be called like functions. They can maintain state, unlike regular functions.
#include <iostream>
struct Adder {
int x;
Adder(int n) : x(n) {}
int operator()(int y) const { return x + y; }
};
int main() {
Adder add5(5);
std::cout << add5(3) << std::endl; // Output: 8
return 0;
}
for_each
std::for_each
is a function in the <algorithm>
header. It's a way to apply a function to each element in a range.
#include <iostream>
#include <vector>
#include <algorithm>
void printDouble(int n) {
std::cout << n * 2 << " ";
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), printDouble);
// Output: 2 4 6 8 10
return 0;
}
Using Lambdas with std::for_each
std::for_each(numbers.begin(), numbers.end(),
[](int n) { std::cout << n * 2 << " "; });
transform
std::transform
from the <algorithm>
is the classic version of the transform
algorithm. It applies a given function to each element in a range and stores the result in another range.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squares(numbers.size());
std::transform(numbers.begin(), numbers.end(), squares.begin(), [](int n){ return n * n; });
for (int n : squares) std::cout << n << " "; // Output: 1 4 9 16 25
return 0;
}
std::ranges::transform
from the <algorithm>
applies a given function to each element in an input range and stores the result in an output range. However, it’s designed to work seamlessly with the concepts of ranges and views.
#include <iostream>
#include <vector>
#include <algorithm> // for std::ranges::transform
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squares(numbers.size()); // Pre-allocate space for the result
std::ranges::transform(numbers, squares.begin(), [](int n) { return n * n; });
for (int n : squares) {
std::cout << n << " "; // Output: 1 4 9 16 25
}
std::cout << std::endl;
return 0;
}
std::views::transform
(from <ranges>
) This is the newer version of transform
introduced in C++20 as part of the ranges library. It creates a view of the transformed range without modifying the original range. Typically used with the pipe (|
) operator for a more functional style.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto squares = numbers | std::views::transform([](int n) { return n * n; });
for (int n : squares) {
std::cout << n << " "; // Output: 1 4 9 16 25
}
std::cout << std::endl;
return 0;
}
filter
std::filter
(from <algorithm>
) This version of std::filter
is older and works with iterators. It's not part of the newer ranges library introduced in C++20. It copies elements that satisfy a predicate to an output iterator.
#include <iostream>
#include <vector>
#include <algorithm> // For std::filter
#include <iterator> // For std::back_inserter
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> evenNumbers;
std::filter(numbers.begin(), numbers.end(),
std::back_inserter(evenNumbers),
[](int n) { return n % 2 == 0; });
for (int n : evenNumbers) {
std::cout << n << " "; // Output: 2 4
}
std::cout << std::endl;
return 0;
}
std::ranges::filter
(from <algorithm>
) This is the newer version introduced with C++20 as part of the ranges library. It works with ranges and views and has a different interface.
#include <iostream>
#include <vector>
#include <algorithm> // For std::ranges::filter
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto evenNumbers = std::ranges::filter(numbers, [](int n) { return n % 2 == 0; });
// ... (copy to a new vector if needed)
}
std::views::filter
This is a range adaptor. It creates a view of the original range that only includes elements satisfying the provided predicate (filter condition). It doesn’t modify the original range. Typically used with the pipe (|
) operator for a more functional style.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto evenNumbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// evenNumbers is a view, numbers remains unchanged
for (int n : evenNumbers) {
std::cout << n << " "; // Output: 2 4
}
std::cout << std::endl;
return 0;
}
std::accumulate
std::accumulate
is a standard algorithm in C++ that iterates through a range of elements and accumulates them into a single value. It's particularly useful for summing up elements of a container like a vector or array.
Basic example:
0
: This is the initial value of the accumulator.
int main() {
vector<int> numbers = {1, 2, 3, 4, 5};
int sum = accumulate(numbers.begin(), numbers.end(), 0);
}
Custom Operations: You can use custom functions as the accumulator operation to perform more complex calculations.
#include <numeric>
#include <vector>
// Custom operation to calculate the product of elements
int product(int a, int b) {
return a * b;
}
int main() {
std::vector<int> numbers = {2, 3, 4, 5};
int product_value = std::accumulate(numbers.begin(), numbers.end(), 1, product);
}
std::reduce
In C++17, the std::reduce
algorithm was introduced to provide a more flexible and efficient way to reduce a range of elements to a single value. It's particularly useful for parallel execution and non-commutative operations.
Basic Usage:
int main() {
vector<int> numbers = {1, 2, 3, 4, 5};
// Summing elements:
int sum = std::reduce(numbers.begin(), numbers.end());
// Custom operation:
auto product = std::reduce(numbers.begin(), numbers.end(), 1, std::multiplies<int>());
// Parallel execution (requires an execution policy):
int parallel_sum = std::reduce(std::execution::par, numbers.begin(), numbers.end());
// ...
}
Example: Custom Reduction Operation
#include <numeric>
#include <vector>
// Custom operation to find the maximum absolute value
int max_abs(int a, int b) {
return std::max(std::abs(a), std::abs(b));
}
int main() {
std::vector<int> numbers = {-2, 5, -8, 3, 10};
int max_absolute_value = std::reduce(numbers.begin(), numbers.end(), 0, max_abs);
// ...
}
std::function
A generic way to represent any callable object (functions, lambdas, functors).
#include <functional>
#include <iostream>
int main() {
std::function<int(int, int)> func = [](int a, int b) { return a + b; };
std::cout << func(2, 3) << std::endl; // Output: 5
return 0;
}
Functional Pipeline
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
// Functional pipeline using lambdas and algorithms
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; }) // Filter even numbers
| std::views::transform([](int n) { return n * n; }) // Square the numbers
| std::views::transform([](int n) { return n + 1; }); // Add 1 to each number
// Print the result
for (int n : result) {
std::cout << n << " "; // Output: 5 17 37
}
std::cout << std::endl;
return 0;
}
Higher-Order Functions
Function Taking Another Function as an Argument
- The
apply
function takes a vector and a function pointer (void (*operation)(int&)
). This function pointer can point to any function that takes an integer reference as an argument and doesn't return anything. - Inside
apply
, theoperation
function is called for each element in the vector. - In
main
, we pass different functions (square
andincrement
) toapply
, demonstrating how a higher-order function can be used to generalize behavior.
#include <iostream>
#include <vector>
#include <algorithm>
// Function to apply an operation to each element in a vector
void apply(std::vector<int>& vec, void (*operation)(int&)) {
for (int& num : vec) {
operation(num);
}
}
// Functions to be used as operations
void square(int& n) { n *= n; }
void increment(int& n) { n++; }
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
apply(numbers, square); // Apply the 'square' function to each element
for (int n : numbers) {
std::cout << n << " "; // Output: 1 4 9 16 25
}
std::cout << std::endl;
apply(numbers, increment); // Apply the 'increment' function
for (int n : numbers) {
std::cout << n << " "; // Output: 2 5 10 17 26
}
std::cout << std::endl;
return 0;
}
Returning a Function from a Function
makeMultiplier
takes an integerfactor
and returns a lambda function that multiplies its input by that factor.std::function<int(int)>
is used to represent the returned function, which takes an integer and returns an integer.- This example shows how higher-order functions can be used to create new functions with customized behavior.
#include <iostream>
#include <functional>
// Function that returns a function
std::function<int(int)> makeMultiplier(int factor) {
return [factor](int n) { return n * factor; };
}
int main() {
auto multiplyBy2 = makeMultiplier(2);
auto multiplyBy5 = makeMultiplier(5);
std::cout << multiplyBy2(10) << std::endl; // Output: 20
std::cout << multiplyBy5(10) << std::endl; // Output: 50
return 0;
}
Using Lambdas for Higher-Order Functions
- This example demonstrates a higher-order lambda function
applyToAll
that takes another function as an argument. - The
auto operation
parameter allows the lambda to accept any callable object (function, lambda, etc.).
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Higher-order lambda that takes a function as an argument
auto applyToAll = [](std::vector<int>& vec, auto operation) {
for (int& num : vec) {
operation(num);
}
};
// Use the higher-order lambda
applyToAll(numbers, [](int& n) { n *= 2; }); // Double each element
for (int n : numbers) {
std::cout << n << " "; // Output: 2 4 6 8 10
}
std::cout << std::endl;
return 0;
}
Happy learning C++ :-)