Menu
Coddy logo textTech

Функторы и лямбда-выражения

Часть раздела Объектно-ориентированное программирование путешествия по 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}
challenge icon

Задание

Легко

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

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

  • Discounts.h: Определите здесь свои функторы скидок и вспомогательные функции.

    Создайте функтор с именем PercentageDiscount, который хранит процент скидки (как целое число). Его operator() должен принимать цену типа double и возвращать цену со скидкой. Например, скидка 20% на $100 должна вернуть $80.

    Создайте еще один функтор с именем FixedDiscount, который хранит фиксированную сумму для вычитания (как double). Его operator() должен принимать цену и возвращать цену за вычетом фиксированной суммы (но не ниже 0).

    Создайте функцию с именем printPrices, которая принимает const std::vector<double>& и выводит все цены, разделенные пробелами, за которыми следует символ новой строки. Форматируйте каждую цену с двумя знаками после запятой.

  • main.cpp: Считайте пять входных значений (каждое на отдельной строке):
    1. Цена первого товара (double)
    2. Цена второго товара (double)
    3. Цена третьего товара (double)
    4. Процент скидки для применения (целое число, например, 20 для 20%)
    5. Фиксированная сумма скидки (double)

    Создайте вектор с тремя ценами и продемонстрируйте оба подхода:

    1. Выведите Original prices: , а затем сами цены
    2. Используйте std::transform с вашим функтором PercentageDiscount, чтобы создать новый вектор цен со скидкой. Выведите After percentage discount: , а затем результаты
    3. Используйте std::transform с вашим функтором FixedDiscount на исходных ценах, чтобы создать еще один вектор. Выведите After fixed discount: , а затем результаты
    4. Используйте std::transform с лямбда-выражением, которое удваивает каждую исходную цену. Выведите Premium prices (doubled): , а затем результаты
    5. Используйте 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;
}
quiz iconПроверьте себя

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

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