Menu
Coddy logo textTech

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

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

Лямбда-выражения, появившиеся в C++11, — это анонимные функции, которые можно определять непосредственно в коде. Хотя вы уже видели простые лямбда-выражения с алгоритмами STL, понимание их полного синтаксиса открывает широкие возможности для захвата переменных и управления доступом к ним.

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

#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 внутри lambda. Ключевое слово 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

Задание

Легко

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

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

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

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

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

    Вам потребуется подключить <functional> и <vector>.

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

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

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

    После добавления всех callbacks вызовите 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: 1
Final multiplier: 7

При входных значениях 42 и 0:

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

Обратите внимание на ключевые особенности: захват по значению сохраняет исходное значение base неизменным, захват по ссылке изменяет фактическую переменную-множитель в main (накапливая изменения при каждом вызове), а счётчик изменяемой лямбды в каждом раунде имеет значение 1. Это происходит потому, что fireAll() перебирает свои callbacks по значению: в каждом раунде вызывается новая копия сохранённого std::function, захваченный счётчик которой начинается с 0 и увеличивается до 1, поэтому собственное состояние сохранённого callback никогда не продвигается.

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

#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 ПО ССЫЛКЕ
    // It should increment multiplier by 1, then print: "Multiplier after increment: [multiplier]"
    
    // TODO: Добавьте MUTABLE лямбду, которая захватывает счётчик (инициализированный значением 0) по значению
    // Она должна увеличивать счётчик и выводить: "Call count: [counter]"
    
    // TODO: Вызовите fireAll(), чтобы вызвать все колбэки
    
    // TODO: Выведите "---" в качестве разделителя
    
    // TODO: Вызовите fireAll() снова
    
    // TODO: Print the final multiplier value: "Final multiplier: [multiplier]"
    
    return 0;
}
quiz iconПроверьте себя

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор C++