Menu
Coddy logo textTech

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

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 85 из 104.

Лямбда-выражения, появившиеся в C++11, представляют собой анонимные функции, которые можно определять непосредственно в коде (inline). Хотя вы уже встречали базовые лямбда-выражения в алгоритмах STL, понимание их полного синтаксиса открывает мощные возможности для захвата переменных и управления способами доступа к ним.

Полный синтаксис лямбда-выражения: [capture](parameters) mutable -> return_type { body }. Список захвата (capture clause) определяет, к каким внешним переменным лямбда-выражение имеет доступ и каким образом:

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    
    auto byValue = [x]() { return x * 2; };        // Копия x
    auto byRef = [&y]() { y += 5; };               // Ссылка на y
    auto allByValue = [=]() { return x + y; };     // Копировать всё
    auto allByRef = [&]() { x++; y++; };           // Захватить всё по ссылке
    auto mixed = [x, &y]() { y += x; };            // Смешанный захват
    
    byRef();
    std::cout << y << "\n";  // 25
}

По умолчанию переменные, захваченные по значению, являются const внутри лямбда-выражения. Ключевое слово mutable позволяет изменять эти копии:

int counter = 0;
auto increment = [counter]() mutable {
    return ++counter;  // Изменяет копию внутри лямбда-выражения
};

std::cout << increment() << "\n";  // 1
std::cout << increment() << "\n";  // 2
std::cout << counter << "\n";      // 0 - оригинал не изменен

В C++14 были добавлены инициализирующие захваты (init captures), позволяющие создавать новые переменные или перемещать объекты в лямбда-выражение:

auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
    return *p;
};  // Владение передано в лямбда-выражение

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

challenge icon

Задание

Легко

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

Вы организуете свой код в трех файлах:

  • EventDispatcher.h: Определите класс EventDispatcher, который управляет обратными вызовами событий.

    Ваш диспетчер должен хранить обратные вызовы, используя std::vector из std::function<void()>. Включите следующие методы:

    • addCallback(std::function<void()> callback) — добавляет обратный вызов в список
    • fireAll() — вызывает все сохраненные обратные вызовы по порядку
    • clear() — удаляет все обратные вызовы

    Вам нужно будет включить <functional> и <vector>.

  • EventDispatcher.cpp: Реализуйте методы для вашего диспетчера. Метод fireAll() должен просто перебирать все обратные вызовы и вызывать каждый из них.
  • main.cpp: Считайте два входных значения:
    1. Базовое число (целое число)
    2. Множитель (целое число)

    Создайте EventDispatcher и продемонстрируйте различные техники захвата лямбда-выражений, добавив три обратных вызова:

    1. Лямбда-выражение, которое захватывает базовое число по значению и выводит: Base value: [base]
    2. Лямбда-выражение, которое захватывает множитель по ссылке, увеличивает его на 1, а затем выводит: Multiplier after increment: [multiplier]
    3. Mutable лямбда-выражение, которое захватывает переменную-счетчик (инициализированную значением 0) по значению, увеличивает ее при каждом вызове и выводит: Call count: [counter]

    После добавления всех обратных вызовов вызовите fireAll() дважды, чтобы увидеть, как ведут себя различные режимы захвата при нескольких вызовах. Между двумя вызовами fireAll() выведите --- в качестве разделителя.

    Наконец, после обоих раундов выведите конечное значение переменной множителя из main, чтобы показать, как захват по ссылке повлиял на нее: Final multiplier: [multiplier]

Например, при входных данных 10 и 5:

Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 2
Final multiplier: 7

При входных данных 42 и 0:

Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 2
Final multiplier: 2

Обратите внимание на ключевые особенности поведения: захват по значению оставляет исходную базу без изменений, захват по ссылке изменяет фактическую переменную множителя в main (накапливая изменения между вызовами), а счетчик mutable лямбда-выражения сохраняет свое состояние между вызовами fireAll(), так как мы вызываем один и тот же объект лямбда-выражения, хранящийся в диспетчере.

Шпаргалка

Лямбда-выражения — это анонимные функции, которые могут быть определены непосредственно в коде. Полный синтаксис выглядит следующим образом:

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

Списки захвата

Список захвата определяет, к каким внешним переменным лямбда-выражение может иметь доступ и каким образом:

int x = 10;
int y = 20;

auto byValue = [x]() { return x * 2; };        // Копия x
auto byRef = [&y]() { y += 5; };               // Ссылка на y
auto allByValue = [=]() { return x + y; };     // Копировать все переменные
auto allByRef = [&]() { x++; y++; };           // Захватить все переменные по ссылке
auto mixed = [x, &y]() { y += x; };            // Смешанный режим

Изменяемые (mutable) лямбда-выражения

По умолчанию переменные, захваченные по значению, являются const внутри тела лямбды. Используйте ключевое слово mutable, чтобы иметь возможность изменять копии:

int counter = 0;
auto increment = [counter]() mutable {
    return ++counter;  // Изменяет копию внутри лямбды
};

std::cout << increment() << "\n";  // 1
std::cout << increment() << "\n";  // 2
std::cout << counter << "\n";      // 0 - оригинал не изменен

Инициализирующий захват (C++14)

Позволяет создавать новые переменные или перемещать объекты в лямбда-выражение:

auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
    return *p;
};  // Владение передано в лямбду

Использование лямбда-выражений с std::function

Вы можете сохранять лямбда-выражения в объектах std::function для реализации обратных вызовов и обработчиков событий:

#include <functional>
#include <vector>

std::vector<std::function<void()>> callbacks;
callbacks.push_back([x]() { /* использование x */ });
callbacks.push_back([&y]() { /* изменение y */ });

Попробуйте сами

#include <iostream>
#include "EventDispatcher.h"

using namespace std;

int main() {
    int base;
    int multiplier;
    cin >> base;
    cin >> multiplier;
    
    EventDispatcher dispatcher;
    
    // TODO: Добавьте лямбда-выражение, которое захватывает base ПО ЗНАЧЕНИЮ
    // Оно должно выводить: "Base value: [base]"
    
    // TODO: Добавьте лямбда-выражение, которое захватывает multiplier ПО ССЫЛКЕ
    // Оно должно увеличивать multiplier на 1, затем выводить: "Multiplier after increment: [multiplier]"
    
    // TODO: Добавьте MUTABLE лямбда-выражение, которое захватывает counter (инициализированный значением 0) по значению
    // Оно должно увеличивать counter и выводить: "Call count: [counter]"
    
    // TODO: Вызовите fireAll(), чтобы запустить все обратные вызовы
    
    // TODO: Выведите "---" в качестве разделителя
    
    // TODO: Снова вызовите fireAll()
    
    // TODO: Выведите конечное значение multiplier: "Final multiplier: [multiplier]"
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование