Функторы и лямбда-выражения
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 74 из 104.
Многие алгоритмы STL принимают вызываемый объект, который настраивает их поведение. Вы уже видели использование лямбда-выражений с std::transform. Давайте изучим как функторы, так и лямбда-выражения — два способа создания вызываемых объектов в C++.
Функтор (функциональный объект) — это класс, который перегружает operator(), позволяя вызывать его экземпляры подобно функциям:
#include <algorithm>
#include <vector>
#include <iostream>
struct MultiplyBy {
int factor;
MultiplyBy(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
int main() {
std::vector<int> nums = {1, 2, 3, 4};
std::vector<int> result(nums.size());
std::transform(nums.begin(), nums.end(), result.begin(), MultiplyBy(3));
// результат: {3, 6, 9, 12}
}Функторы могут сохранять состояние между вызовами, чего не могут обычные функции. Однако определение класса для простых операций является избыточным. Лямбда-выражения предоставляют лаконичную альтернативу:
int factor = 3;
std::transform(nums.begin(), nums.end(), result.begin(),
[factor](int x) { return x * factor; });Синтаксис лямбда-выражения — [capture](parameters) { body }. Предложение захвата определяет, к каким внешним переменным лямбда-выражение может иметь доступ. Используйте [=] для захвата всех переменных по значению, [&] для захвата всех переменных по ссылке или перечислите конкретные переменные, такие как [factor] или [&factor].
Лямбда-выражения особенно полезны для разовых операций с такими алгоритмами, как сортировка по пользовательским критериям:
std::vector<int> nums = {5, -2, 8, -1};
std::sort(nums.begin(), nums.end(),
[](int a, int b) { return std::abs(a) < std::abs(b); });
// Отсортировано по абсолютному значению: {-1, -2, 5, 8}Задание
ЛегкоДавайте создадим калькулятор цен, который демонстрирует использование функторов и лямбда-выражений для применения различных стратегий скидок к ценам на товары.
Вы организуете свой код в двух файлах:
Discounts.h: Определите здесь свои функторы скидок и вспомогательные функции.Создайте функтор с именем
PercentageDiscount, который хранит процент скидки (как целое число). Егоoperator()должен принимать цену типаdoubleи возвращать цену со скидкой. Например, скидка 20% на $100 должна вернуть $80.Создайте еще один функтор с именем
FixedDiscount, который хранит фиксированную сумму для вычитания (какdouble). Егоoperator()должен принимать цену и возвращать цену за вычетом фиксированной суммы (но не ниже 0).Создайте функцию с именем
printPrices, которая принимаетconst std::vector<double>&и выводит все цены, разделенные пробелами, за которыми следует символ новой строки. Форматируйте каждую цену с двумя знаками после запятой.main.cpp: Считайте пять входных значений (каждое на отдельной строке):- Цена первого товара (double)
- Цена второго товара (double)
- Цена третьего товара (double)
- Процент скидки для применения (целое число, например, 20 для 20%)
- Фиксированная сумма скидки (double)
Создайте вектор с тремя ценами и продемонстрируйте оба подхода:
- Выведите
Original prices:, а затем сами цены - Используйте
std::transformс вашим функторомPercentageDiscount, чтобы создать новый вектор цен со скидкой. ВыведитеAfter percentage discount:, а затем результаты - Используйте
std::transformс вашим функторомFixedDiscountна исходных ценах, чтобы создать еще один вектор. ВыведитеAfter fixed discount:, а затем результаты - Используйте
std::transformс лямбда-выражением, которое удваивает каждую исходную цену. ВыведитеPremium prices (doubled):, а затем результаты - Используйте
std::sortс лямбда-выражением, чтобы отсортировать исходные цены в порядке убывания. ВыведитеSorted (high to low):, а затем отсортированные цены
Например, при входных данных 100.00, 50.00, 75.00, 20 и 15.00:
Original prices: 100.00 50.00 75.00
After percentage discount: 80.00 40.00 60.00
After fixed discount: 85.00 35.00 60.00
Premium prices (doubled): 200.00 100.00 150.00
Sorted (high to low): 100.00 75.00 50.00 Эта задача позволяет сравнить функторы (которые сохраняют состояние, например, сумму скидки) с лямбда-выражениями (которые захватывают переменные для быстрых встроенных операций). Оба подхода легко работают с алгоритмами STL, такими как std::transform и std::sort.
Шпаргалка
Алгоритмы STL принимают вызываемые объекты для настройки своего поведения. Два распространенных подхода — это функторы и лямбда-выражения.
Функтор — это класс, который перегружает operator(), позволяя вызывать его экземпляры подобно функциям:
struct MultiplyBy {
int factor;
MultiplyBy(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
std::transform(nums.begin(), nums.end(), result.begin(), MultiplyBy(3));Функторы могут сохранять состояние между вызовами, но требуют многословных определений классов.
Лямбда-выражения предоставляют краткую альтернативу с синтаксисом [capture](parameters) { body }:
int factor = 3;
std::transform(nums.begin(), nums.end(), result.begin(),
[factor](int x) { return x * factor; });Список захвата (capture clause) определяет, к каким внешним переменным лямбда-выражение может иметь доступ:
[=]— захват всех переменных по значению[&]— захват всех переменных по ссылке[factor]— захват конкретной переменной по значению[&factor]— захват конкретной переменной по ссылке
Лямбды хорошо работают с такими алгоритмами, как std::sort, для задания пользовательских критериев сортировки:
std::sort(nums.begin(), nums.end(),
[](int a, int b) { return std::abs(a) < std::abs(b); });Попробуйте сами
#include <iostream>
#include <vector>
#include <algorithm>
#include "Discounts.h"
int main() {
// Считать входные данные
double price1, price2, price3;
int percentageDiscount;
double fixedDiscount;
std::cin >> price1;
std::cin >> price2;
std::cin >> price3;
std::cin >> percentageDiscount;
std::cin >> fixedDiscount;
// Создать вектор с тремя ценами
std::vector<double> prices = {price1, price2, price3};
// TODO: Вывести "Original prices:", а затем сами цены, используя printPrices
// TODO: Использовать std::transform с функтором PercentageDiscount
// Создать новый вектор для результатов
// Вывести "After percentage discount:", а затем результаты
// TODO: Использовать std::transform с функтором FixedDiscount для исходных цен
// Создать новый вектор для результатов
// Вывести "After fixed discount:", а затем результаты
// TODO: Использовать std::transform с лямбда-выражением, которое удваивает каждую исходную цену
// Создать новый вектор для результатов
// Вывести "Premium prices (doubled):", а затем результаты
// TODO: Использовать std::sort с лямбда-выражением для сортировки исходных цен по убыванию
// Вывести "Sorted (high to low):", а затем отсортированные цены
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Итоги — Обобщенный контейнер