Функциональное программирование в C++: от лямбд до композиции
Функциональное программирование (ФП) — это парадигма, в которой вычисления строятся на применении чистых функций, неизменяемости данных и отсутствии побочных эффектов. Хотя C++ изначально был мультипарадигменным языком, его современные стандарты (C++11/14/17/20) внедрили мощные инструменты для ФП: лямбда-выражения, std::function, алгоритмы STL, корутины и даже поддержку монадических паттернов. В этой статье мы разберём, как писать функциональный код на C++, не теряя производительности.
1. Основы функционального стиля: лямбды и std::function
Лямбда-выражения — сердце ФП в C++. Они позволяют создавать анонимные функции «на лету», захватывая контекст.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// Лямбда для фильтрации чётных чисел
auto is_even = [](int x) { return x % 2 == 0; };
// Использование с алгоритмом std::copy_if
std::vector<int> evens;
std::copy_if(nums.begin(), nums.end(),
std::back_inserter(evens), is_even);
for (int n : evens) std::cout << n << " "; // 2 4
return 0;
}
Ключевые моменты лямбд:
- Захват —
[=](по значению),[&](по ссылке),[this](для методов класса). - Тип — каждая лямбда имеет уникальный тип, поэтому для передачи в функцию используют
std::functionили шаблоны. - Возвращаемое значение — выводится автоматически, если не указано иное.
std::function — обёртка для callable-объектов (функции, лямбды, функторы). Удобна для хранения и передачи, но добавляет накладные расходы (виртуальный вызов).
#include <functional>
void apply_twice(std::function<int(int)> f, int x) {
std::cout << f(f(x)) << std::endl; // Композиция
}
int main() {
auto square = [](int x) { return x * x; };
apply_twice(square, 3); // (3^2)^2 = 81
return 0;
}
2. Алгоритмы STL как основа функциональных цепочек
STL-алгоритмы (std::transform, std::copy_if, std::accumulate и др.) идеально ложатся на функциональный стиль. Они не изменяют контейнеры напрямую, а принимают итераторы и возвращают результаты.
Пример: фильтрация и преобразование (map/filter)
#include <numeric>
#include <ranges> // C++20
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6};
// Классический подход (C++11)
std::vector<int> filtered;
std::copy_if(data.begin(), data.end(),
std::back_inserter(filtered),
[](int x) { return x > 3; });
std::vector<int> result;
std::transform(filtered.begin(), filtered.end(),
std::back_inserter(result),
[](int x) { return x * 2; });
// Современный подход с ranges (C++20)
auto view = data
| std::views::filter([](int x) { return x > 3; })
| std::views::transform([](int x) { return x * 2; });
for (int v : view) std::cout << v << " "; // 8 10 12
return 0;
}
Свёртка (fold) с std::accumulate
#include <numeric>
int main() {
std::vector<int> nums = {1, 2, 3, 4};
// Сумма с начальным значением 0
int sum = std::accumulate(nums.begin(), nums.end(), 0,
[](int a, int b) { return a + b; });
// Произведение с начальным значением 1
int prod = std::accumulate(nums.begin(), nums.end(), 1,
std::multiplies<int>());
std::cout << "Sum: " << sum << ", Prod: " << prod; // Sum: 10, Prod: 24
return 0;
}
3. Композиция функций и каррирование
Композиция — объединение двух или более функций в одну. В C++ её можно реализовать через лямбды или вспомогательные шаблоны.
#include <functional>
// Вспомогательная функция для композиции
template<typename F, typename G>
auto compose(F f, G g) {
return [=](auto x) { return f(g(x)); };
}
int main() {
auto add_one = [](int x) { return x + 1; };
auto square = [](int x) { return x * x; };
auto add_then_square = compose(square, add_one);
std::cout << add_then_square(2); // (2+1)^2 = 9
// С помощью std::bind (менее читаемо)
auto bind_example = std::bind(square, std::bind(add_one, std::placeholders::_1));
std::cout << bind_example(2); // 9
return 0;
}
Каррирование — превращение функции от нескольких аргументов в последовательность функций от одного аргумента. Реализуется через вложенные лямбды:
auto curried_add = [](int a) {
return [a](int b) {
return a + b;
};
};
int main() {
auto add_five = curried_add(5);
std::cout << add_five(3); // 8
return 0;
}