Перейти к содержимому

Лямбды и функциональное программирование

Освойте лямбды C++ — списки захвата, mutable-лямбды, обобщённые лямбды и их использование в качестве предикатов алгоритмов. Изучите std::function, std::invoke и паттерн IIFE.

Лямбда-выражения

Лямбда-выражение создаёт анонимный функциональный объект (замыкание) прямо в месте использования. Лямбды — основной способ передачи поведения в алгоритмы STL, обратные вызовы и обработчики событий.

Полный синтаксис:

[captures](parameters) mutable -> return_type { body }

- Захваты — переменные из охватывающей области видимости для использования внутри лямбды
- Параметры — как обычные параметры функции
- mutable — позволяет изменять захваченные по значению переменные
- Возвращаемый тип — обычно выводится автоматически
- Тело — тело функции

Лямбды компилируются в сгенерированные компилятором функциональные объекты (структуры с operator()) — они имеют нулевые накладные расходы по сравнению с написанными вручную функторами.

Базовый синтаксис лямбд и захваты

Список захвата — определяющая особенность лямбд. Он определяет, к каким переменным охватывающей области лямбда может обращаться и каким образом.

lambda_captures.cpp
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 5, 3, 8, 2, 9, 4, 7, 6};

    // Simplest lambda — no captures
    std::sort(nums.begin(), nums.end(),
              [](int a, int b) { return a > b; });  // descending

    // Capture by value [=] — copies variables
    int threshold = 5;
    auto above = std::count_if(nums.begin(), nums.end(),
                               [threshold](int x) { return x > threshold; });
    std::cout << "Above " << threshold << ": " << above << '\n';

    // Capture by reference [&] — references variables
    int sum = 0;
    std::for_each(nums.begin(), nums.end(),
                  [&sum](int x) { sum += x; });
    std::cout << "Sum: " << sum << '\n';

    // Mixed captures
    int min_val = 3, max_val = 7;
    auto in_range = std::count_if(nums.begin(), nums.end(),
        [min_val, &max_val](int x) { return x >= min_val && x <= max_val; });
    // min_val captured by value, max_val captured by reference

    // Capture all by value [=] or all by reference [&]
    auto print_all = [&]() {  // captures everything by reference
        for (int n : nums) std::cout << n << ' ';
        std::cout << '\n';
    };
    print_all();

    // Init capture (C++14) — create new variables in the capture
    auto counter = [count = 0]() mutable { return ++count; };
    std::cout << counter() << '\n';  // 1
    std::cout << counter() << '\n';  // 2
    std::cout << counter() << '\n';  // 3
}

Обобщённые и шаблонные лямбды

C++14 ввёл обобщённые лямбды с параметрами auto. C++20 расширил это явными списками параметров шаблонов, предоставляя лямбдам полную мощь шаблонов.

generic_lambdas.cpp
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <type_traits>

int main() {
    // C++14: generic lambda — auto parameters
    auto print = [](const auto& x) {
        std::cout << x << '\n';
    };
    print(42);           // int
    print(3.14);         // double
    print("hello");      // const char*

    // C++14: generic lambda for comparisons
    auto max_of = [](const auto& a, const auto& b) {
        return (a > b) ? a : b;
    };
    std::cout << max_of(3, 7) << '\n';             // 7
    std::cout << max_of("abc", "xyz") << '\n';     // xyz (lexicographic)

    // C++20: template lambda — explicit template parameters
    auto add = []<typename T>(T a, T b) -> T {
        return a + b;
    };
    std::cout << add(1, 2) << '\n';                // 3
    std::cout << add(1.5, 2.5) << '\n';            // 4.0

    // C++20: constrained template lambda with concepts
    auto integral_only = []<std::integral T>(T x) {
        return x * 2;
    };
    std::cout << integral_only(5) << '\n';         // 10
    // integral_only(3.14);  // ERROR: double doesn't satisfy std::integral

    // Using generic lambdas with algorithms
    std::vector<std::string> words = {"banana", "apple", "cherry"};
    std::sort(words.begin(), words.end(),
              [](const auto& a, const auto& b) {
                  return a.size() < b.size();  // sort by length
              });
    for (const auto& w : words) std::cout << w << ' ';  // apple banana cherry
    std::cout << '\n';
}

std::function и std::invoke

std::function — это обёртка с затиранием типа для вызываемых объектов, которая может содержать любой вызываемый объект с подходящей сигнатурой — лямбды, указатели на функции, функторы, указатели на функции-члены. std::invoke — это единообразный способ вызова любого вызываемого объекта.

std_function.cpp
#include <functional>
#include <iostream>
#include <string>
#include <vector>

// Regular function
int add(int a, int b) { return a + b; }

struct Multiplier {
    int factor;
    int operator()(int x) const { return x * factor; }
    int multiply(int x) const { return x * factor; }
};

int main() {
    // std::function can hold different callable types
    std::function<int(int, int)> op;

    op = add;                                        // function pointer
    std::cout << op(3, 4) << '\n';                   // 7

    op = [](int a, int b) { return a * b; };         // lambda
    std::cout << op(3, 4) << '\n';                   // 12

    // Store callables in a collection
    std::vector<std::function<int(int)>> transforms;
    transforms.push_back([](int x) { return x + 1; });
    transforms.push_back([](int x) { return x * 2; });
    transforms.push_back(Multiplier{10});            // functor

    int val = 5;
    for (auto& fn : transforms) {
        val = fn(val);
    }
    std::cout << val << '\n';  // ((5+1)*2)*10 = 120

    // std::invoke — uniform call syntax for any callable
    Multiplier m{3};
    std::cout << std::invoke(add, 10, 20)       << '\n';  // 30
    std::cout << std::invoke(m, 7)              << '\n';  // 21
    std::cout << std::invoke(&Multiplier::multiply, m, 7) << '\n';  // 21
    std::cout << std::invoke(&Multiplier::factor, m)      << '\n';  // 3 (data member)
}

Паттерн IIFE

IIFE (Immediately Invoked Function Expression — немедленно вызываемое функциональное выражение) — это паттерн, заимствованный из JavaScript. Вы определяете лямбду и сразу же вызываете её. Это полезно для сложной инициализации const-переменных:

const auto config = [&]() {
    Config c;
    c.host = read_env("HOST");
    c.port = parse_int(read_env("PORT"));
    if (c.port <= 0) c.port = 8080;
    c.debug = is_debug_build();
    return c;
}();  // <-- обратите внимание на () — вызывается немедленно

Без IIFE вам пришлось бы либо использовать не-const переменную (теряя гарантии неизменяемости), либо создавать отдельную фабричную функцию. IIFE сохраняет логику инициализации локальной, а результат — const.

Подводный камень

Захват по ссылке опасен, когда лямбда живёт дольше захваченной переменной:

std::function<int()> make_counter() {
    int count = 0;
    return [&count]() { return ++count; };  // ОШИБКА: count уничтожен!
}

Возвращённая лямбда содержит ссылку на локальную переменную, которой больше не существует. Это неопределённое поведение. Исправьте, захватив по значению (с mutable при необходимости):

std::function<int()> make_counter() {
    int count = 0;
    return [count]() mutable { return ++count; };  // OK: владеет копией
}

Правило: захватывайте по ссылке для короткоживущих лямбд (предикаты алгоритмов), захватывайте по значению для долгоживущих лямбд (обратные вызовы, хранимые в структурах данных).

Лучшая практика

std::function имеет накладные расходы: выделяет память (для больших замыканий), использует виртуальную диспетчеризацию и предотвращает инлайнинг. Для предикатов алгоритмов и других случаев, когда вызываемый объект используется немедленно, передавайте лямбду напрямую — компилятор может полностью встроить её.

Используйте std::function только когда нужно:
- Хранить вызываемые объекты для последующего использования
- Затереть тип различных вызываемых объектов в единый контейнер
- Передавать вызываемые объекты через границы ABI

В шаблонах используйте auto или концепты для приёма вызываемых объектов без затирания типа:

template<std::invocable<int> F>
void apply(F&& fn, int value) {
    fn(value);  // без накладных расходов std::function
}
Ключевые выводы
  • Лямбды — анонимные функциональные объекты с нулевыми накладными расходами — свободно используйте их с алгоритмами STL
  • Захватывайте по значению [x] для безопасности с долгоживущими лямбдами; захватывайте по ссылке [&x] для короткоживущих
  • Обобщённые лямбды C++14 (параметры auto) и шаблонные лямбды C++20 дают полную мощь обобщённого программирования
  • std::function затирает тип вызываемых объектов, но имеет накладные расходы — предпочитайте auto или шаблоны, когда возможно
  • Паттерн IIFE [&]() { ... }() позволяет выполнять сложную инициализацию const-переменных
  • Инициализирующие захваты ([x = expr]) позволяют создавать новые переменные в замыкании (C++14)

Тест — проверьте свои знания

(15 XP)

1. Что делает ключевое слово `mutable` в лямбде?

2. В чём опасность захвата локальной переменной по ссылке в лямбде, которая возвращается из функции?

3. Что демонстрирует `[count = 0]() mutable { return ++count; }`?