Функциональное программирование в C++: продвинутый уровень

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

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

Начать курс

Функциональное программирование в C++: продвинутый уровень

Функциональное программирование (ФП) — это парадигма, в которой вычисления представляются как вычисление математических функций, избегая изменения состояния и изменяемых данных. Хотя C++ изначально был объектно-ориентированным и процедурным языком, начиная со стандарта C++11 он получил мощные инструменты для ФП: лямбда-выражения, std::function, std::bind, а в C++17 добавилась поддержка std::optional и std::variant, которые позволяют реализовывать монадические паттерны. В этой статье мы рассмотрим продвинутые техники функционального программирования на C++: композицию функций, каррирование, работу с монадами и создание чистых функций с помощью современных возможностей языка.

1. Лямбда-выражения и чистые функции

Лямбда-выражения — основа функционального стиля в C++. Они позволяют создавать анонимные функции на месте, что идеально для алгоритмов из <algorithm>. Однако для настоящего ФП важно, чтобы лямбды были чистыми — то есть не имели побочных эффектов и не зависели от внешнего изменяемого состояния.

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Чистая лямбда: только входные данные, только выход
    auto square = [](int x) { return x * x; };

    // Применяем transform без побочных эффектов
    std::vector<int> squared;
    std::transform(numbers.begin(), numbers.end(),
                   std::back_inserter(squared), square);

    for (int n : squared) std::cout << n << " "; // 1 4 9 16 25
    return 0;
}

Для продвинутого использования стоит избегать захвата по ссылке ([&]), если это не критично. Захват по значению ([=]) или явное перечисление захватов делает лямбду более предсказуемой и тестируемой.

2. Композиция функций с std::function и операторами

Композиция — это объединение двух или более функций в одну. В C++ можно реализовать композицию с помощью шаблонов и std::function. Рассмотрим обобщённый композитор:

#include <functional>
#include <iostream>

template <typename F, typename G>
auto compose(F f, G g) {
    return [f, g](auto x) {
        return f(g(x));
    };
}

int main() {
    auto addOne = [](int x) { return x + 1; };
    auto multiplyByTwo = [](int x) { return x * 2; };

    auto addThenMultiply = compose(multiplyByTwo, addOne);
    std::cout << addThenMultiply(5) << std::endl; // (5+1)*2 = 12

    // Композиция нескольких функций
    auto triple = compose(multiplyByTwo, compose(addOne, addOne));
    std::cout << triple(5) << std::endl; // ((5+1)+1)*2 = 14
    return 0;
}

Для более сложных цепочек можно использовать конвейерный оператор (наподобие |> в других языках). В C++20 появилась возможность перегружать operator| для создания удобного синтаксиса:

#include <functional>
#include <iostream>

template <typename F>
class Pipeable {
    F f;
public:
    Pipeable(F func) : f(func) {}
    template <typename T>
    auto operator()(T x) const { return f(x); }
};

template <typename T, typename F>
auto operator|(T x, Pipeable<F> p) {
    return p(x);
}

int main() {
    auto addOne = Pipeable([](int x) { return x + 1; });
    auto square = Pipeable([](int x) { return x * x; });

    int result = 5 | addOne | square; // (5+1)^2 = 36
    std::cout << result << std::endl;
    return 0;
}

Такой подход делает код декларативным и легко читаемым.

3. Каррирование и частичное применение

Каррирование — преобразование функции от нескольких аргументов в последовательность функций от одного аргумента. В C++ это можно реализовать через вложенные лямбды или через std::bind. Рассмотрим современный способ с помощью шаблонов и auto (C++14):

#include <iostream>
#include <functional>

// Каррированная функция сложения
auto curry_add = [](int a) {
    return [a](int b) {
        return [a, b](int c) {
            return a + b + c;
        };
    };
};

int main() {
    auto add5 = curry_add(5);
    auto add5and3 = add5(3);
    std::cout << add5and3(2) << std::endl; // 5+3+2 = 10

    // Частичное применение через std::bind
    auto multiply = [](int x, int y, int z) { return x * y * z; };
    auto multiplyBy2 = std::bind(multiply, 2, std::placeholders::_1, std::placeholders::_2);
    std::cout << multiplyBy2(3, 4) << std::endl; // 2*3*4 = 24
    return 0;
}

Каррирование особенно полезно при работе с алгоритмами, где нужно передать частично настроенную функцию, или при создании фабрик функций.

4. Монады и функторы: std::optional и std::expected

Монада — это абстракция, позволяющая последовательно применять функции к значениям, обёрнутым в контекст (например, возможное отсутствие значения или ошибку). В C++ монадический подход реализуется через std::optional (C++17) и std::expected (C++23, доступен в экспериментальной реализации).

Рассмотрим пример с std::optional и функцией and_then (аналог flatMap):

#include <iostream>
#include <optional>
#include <string>

// Монадический and_then для optional
template <typename T, typename F>
auto and_then(std::optional<T> opt, F f) -> decltype(f(*opt)) {
    if (opt) return f(*opt);
    return std::nullopt;
}

std                

Блоги

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