Функциональное программирование в C++: от А до Я
Многие разработчики, знакомые с Python или JavaScript, привыкли к мощным инструментам функционального программирования: лямбды, map, filter, reduce. Но знаете ли вы, что C++ предоставляет не менее гибкие возможности для функционального стиля? В этой статье мы разберём функциональное программирование в C++ от базовых концепций до продвинутых техник. Вы узнаете, как писать более лаконичный, безопасный и выразительный код, используя современные возможности C++11/14/17.
1. Основы функционального подхода: лямбда-выражения и std::function
Функциональное программирование в C++ начинается с лямбда-выражений. Они позволяют создавать анонимные функции прямо в точке использования. Синтаксис лямбды прост:
// Базовая лямбда
auto sum = [](int a, int b) -> int { return a + b; };
std::cout << sum(5, 3); // 8
// Лямбда с захватом переменных
int multiplier = 3;
auto multiply = [multiplier](int x) { return x * multiplier; };
std::cout << multiply(7); // 21
Ключевая особенность лямбд — захват контекста. Вы можете захватывать переменные по значению ([=]), по ссылке ([&]) или комбинированно. Это основа замыканий, которые являются фундаментом функционального стиля.
Для хранения и передачи лямбд используется std::function — полиморфная обёртка для вызываемых объектов:
#include <functional>
std::function<int(int, int)> operation;
operation = [](int a, int b) { return a + b; };
std::cout << operation(10, 20); // 30
operation = [](int a, int b) { return a * b; };
std::cout << operation(10, 20); // 200
Совет: Используйте auto для лямбд, если не требуется хранить их как тип. std::function добавляет небольшие накладные расходы, поэтому его стоит применять только при необходимости полиморфизма.
2. Алгоритмы STL: map, filter, reduce своими руками
В C++ нет встроенных функций map, filter, reduce как в Python, но STL предоставляет мощные алгоритмы, которые их заменяют. Рассмотрим основные:
2.1. Map — преобразование элементов
Вместо map используйте std::transform:
#include <algorithm>
#include <vector>
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; });
// squares: {1, 4, 9, 16, 25}
2.2. Filter — фильтрация по условию
Аналог filter — std::copy_if или std::remove_if (с последующим удалением):
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
std::vector<int> evens;
std::copy_if(numbers.begin(), numbers.end(),
std::back_inserter(evens),
[](int n) { return n % 2 == 0; });
// evens: {2, 4, 6}
2.3. Reduce — свёртка коллекции
Для reduce используйте std::accumulate из <numeric>:
#include <numeric>
std::vector<int> numbers = {1, 2, 3, 4, 5};
int sum = std::accumulate(numbers.begin(), numbers.end(), 0,
[](int acc, int n) { return acc + n; });
// sum = 15
Продвинутый пример: Комбинируем map, filter и reduce для обработки данных:
std::vector<std::string> names = {"Alice", "Bob", "Charlie", "David"};
// Получаем сумму длин имён, начинающихся на 'A' или 'C'
int result = std::accumulate(names.begin(), names.end(), 0,
[](int acc, const std::string& name) {
if (name[0] == 'A' || name[0] == 'C') {
return acc + name.length();
}
return acc;
});
// result = 5 (Alice) + 7 (Charlie) = 12
3. Функторы и каррирование в C++
Функторы — это объекты, которые ведут себя как функции благодаря перегрузке оператора (). Они хранят состояние и могут быть мощнее лямбд:
class Adder {
private:
int base;
public:
Adder(int b) : base(b) {}
int operator()(int x) const { return base + x; }
};
Adder add10(10);
std::cout << add10(5); // 15
// Использование с алгоритмами
std::vector<int> v = {1, 2, 3};
std::transform(v.begin(), v.end(), v.begin(), Adder(100));
// v: {101, 102, 103}
Каррирование (currying) в C++ можно реализовать через вложенные лямбды или шаблоны:
// Каррирование через вложенные лямбды
auto multiply = [](int a) {
return [a](int b) {
return a * b;
};
};
auto multiply_by_2 = multiply(2);
std::cout << multiply_by_2(5); // 10
std::cout << multiply_by_2(10); // 20
// Более сложный пример с тремя аргументами
auto add_three = [](int a) {
return [a](int b) {
return [a, b](int c) {
return a + b + c;
};
};
};
std::cout << add_three(1)(2)(3); // 6
Важно: Каррирование делает код более гибким, но может снизить читаемость. Используйте его осмысленно, например, для создания конфигурируемых функций.
4. Продвинутые техники: range-v3 и C++20
Современный C++ (C++20) предлагает концепцию Ranges, которая делает функциональные цепочки более элегантными