Функциональное программирование в C++: от А до Я — практическое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в 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, которая делает функциональные цепочки более элегантными

Блоги

Рекомендаций по книгам