Лямбда-выражения в деталях
Часть раздела Объектно-ориентированное программирование путешествия по 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;
}; // Владение передано в лямбда-выражениеЛямбда-выражения особенно полезны в ООП, когда вам нужно передать поведение в качестве параметра — для обратных вызовов, пользовательских компараторов или обработчиков событий — без определения отдельных объектов-функций.
Задание
ЛегкоДавайте создадим систему обработки событий, которая демонстрирует возможности лямбда-выражений с различными режимами захвата. Вы создадите простой диспетчер событий, который хранит и вызывает обратные вызовы, демонстрируя, как лямбда-выражения могут захватывать внешнее состояние различными способами.
Вы организуете свой код в трех файлах:
EventDispatcher.h: Определите классEventDispatcher, который управляет обратными вызовами событий.Ваш диспетчер должен хранить обратные вызовы, используя
std::vectorизstd::function<void()>. Включите следующие методы:addCallback(std::function<void()> callback)— добавляет обратный вызов в списокfireAll()— вызывает все сохраненные обратные вызовы по порядкуclear()— удаляет все обратные вызовы
Вам нужно будет включить
<functional>и<vector>.EventDispatcher.cpp: Реализуйте методы для вашего диспетчера. МетодfireAll()должен просто перебирать все обратные вызовы и вызывать каждый из них.main.cpp: Считайте два входных значения:- Базовое число (целое число)
- Множитель (целое число)
Создайте
EventDispatcherи продемонстрируйте различные техники захвата лямбда-выражений, добавив три обратных вызова:- Лямбда-выражение, которое захватывает базовое число по значению и выводит:
Base value: [base] - Лямбда-выражение, которое захватывает множитель по ссылке, увеличивает его на 1, а затем выводит:
Multiplier after increment: [multiplier] - 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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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