Functional programming in c++

Dilip Kumar
8 min readOct 4, 2024

--

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, the operation function is called for each element in the vector.
  • In main, we pass different functions (square and increment) to apply, 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 integer factor 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++ :-)

--

--

Dilip Kumar
Dilip Kumar

Written by Dilip Kumar

With 18+ years of experience as a software engineer. Enjoy teaching, writing, leading team. Last 4+ years, working at Google as a backend Software Engineer.

Responses (1)