Лямбда-выражения в деталях
Часть раздела Объектно-ориентированное программирование путешествия по 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;
}; // Владение передано в лямбдуЛямбда-выражения особенно полезны в ООП, когда нужно передать поведение в качестве параметра — для обратных вызовов, пользовательских компараторов или обработчиков событий — без определения отдельных объектов-функций.
Задание
ЛегкоДавайте создадим систему обработчиков событий, демонстрирующую возможности лямбда-выражений с различными режимами захвата. Вы создадите простой диспетчер событий, который хранит и вызывает 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: Считайте два входных значения:- Число base (целое)
- Множитель (целое)
Создайте
EventDispatcherи продемонстрируйте различные способы захвата в лямбдах, добавив три callback:- Лямбда, которая захватывает число base по значению и выводит:
Base value: [base] - Лямбда, которая захватывает множитель по ссылке, увеличивает его на 1, а затем выводит:
Multiplier after increment: [multiplier] - Изменяемая лямбда, которая захватывает переменную-счётчик (инициализированную значением 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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер12Возможности современного C++
Семантика перемещения и RvaluesИдеальная передача (Perfect Forwarding)Лямбда-выражения в деталяхstd::function и std::bindconstexpr и constevalСтруктурированные привязкиoptional, variant, anyПотренируйтесь самостоятельно: Онлайн-компилятор C++