C++

Лямбда-вирази

Вивчіть лямбда-вирази у C++11 — анонімні функції, що визначаються прямо в місці використання. Синтаксис, типи, auto-параметри, trailing-тип повернення, std::function та функтори STL.

Лямбда-вирази

Проблема: іменовані функції для одноразового використання

У попередній статті ми вивчили вказівники на функції і callback-патерн. Це відкрило нам можливість передавати функцію як аргумент — наприклад, стратегію порівняння у функцію сортування. Але уявіть таку ситуацію: вам потрібно знайти перший рядок у масиві, що містить підрядок "nut". Ви пишете функцію:

BeforeLambda.cpp
#include <iostream>
#include <algorithm>

// Функція визначена в глобальній області — хоча потрібна лише раз і лише тут
bool containsNut(const char* str)
{
    // Пошук підрядка через цикл — спрощений варіант
    int i = 0;
    int nutLen = 3; // "nut"
    const char* target = "nut";

    while (str[i] != '\0')
    {
        if (str[i] == target[0] && str[i + 1] == target[1] && str[i + 2] == target[2])
            return true;
        ++i;
    }

    return false;
}

int main()
{
    const char* fruits[] = { "apple", "banana", "walnut", "lemon" };

    return 0;
}

Зверніть на проблеми цього підходу:

  • containsNut визначена в глобальній області — хоча вона потрібна лише в одному місці.
  • Її ім'я не несе жодної цінності — треба дивитися тіло, щоб зрозуміти, що вона робить.
  • Між місцем визначення і місцем використання може бути десятки рядків коду — навігація ускладнюється.
  • Простір імен засмічується функцією, яка більше ніде не використовуватиметься.

Саме для вирішення цих проблем у стандарті C++11 з'явилися лямбда-вирази — синтаксис для визначення анонімної функції прямо в місці її використання.

Передумови. Стаття є прямим продовженням статті про вказівники на функції (стаття 24). Знання std::function, auto і загального розуміння callback-патерну є обов'язковими.

Синтаксис лямбда-виразу

Загальна форма лямбда-виразу:

[ capture ] ( parameters ) -> ReturnType { body }
ЧастинаОбов'язкова?Призначення
[ capture ]✅ Так (може бути порожньою [])Захоплення зовнішніх змінних (стаття 26)
( parameters )❌ Можна опустити, якщо параметрів немаєПараметри функції
-> ReturnType❌ ОпціональноЯвний тип повернення (trailing return type)
{ body }✅ ТакТіло функції

Найпростіша можлива лямбда — порожня, без параметрів і без тіла:

[]() {}; // мінімальний синтаксис: порожній capture, порожні параметри, порожнє тіло

Більш практичний приклад — лямбда, що приймає два числа і повертає їхню суму:

BasicLambda.cpp
#include <iostream>

int main()
{
    // Лямбда визначається і одразу зберігається у змінній
    auto add = [](int a, int b)
    {
        return a + b; // тип повернення виводиться автоматично: int
    };

    std::cout << add(3, 5)  << '\n'; // 8
    std::cout << add(10, 7) << '\n'; // 17

    return 0;
}
./BasicLambda
$ ./BasicLambda
8
17

Розбір: auto add = [](int a, int b) { return a + b; }; — читається: «add — це лямбда без capture, що приймає два int і повертає їхню суму». Тип add компілятор виводить сам — він є унікальним і є внутрішнім для компілятора. Саме тому ми використовуємо auto.


Лямбда vs іменована функція: порівняння

Повернімося до задачі пошуку підрядка. Порівняємо два підходи поруч:

#include <iostream>

// Визначена в глобальній області, далеко від місця використання
bool isLong(const char* str)
{
    int len = 0;
    while (str[len] != '\0')
        ++len;
    return len > 5;
}

int main()
{
    const char* words[] = { "hi", "hello", "morning", "bye" };

    // ... 50 рядків коду між визначенням і використанням ...

    for (int i = 0; i < 4; ++i)
    {
        if (isLong(words[i]))
            std::cout << words[i] << '\n';
    }

    return 0;
}

Переваги варіанту з лямбдою:

  • Немає забруднення глобального простору імен.
  • Визначення поруч з місцем використання — код читається лінійно.
  • Одразу зрозуміло, що isLong — це локальна допоміжна логіка.

Лямбди як аргументи: прямо у виклику

Лямбда не зобов'язана зберігатися у змінній. Її можна передати прямо у виклик функції як аргумент:

InlineCallback.cpp
#include <iostream>

void forEach(int* array, int size, void (*action)(int))
{
    for (int i = 0; i < size; ++i)
        action(array[i]);
}

int main()
{
    int numbers[] = { 1, 2, 3, 4, 5 };

    // Лямбда прямо у виклику — без імені, без окремого визначення
    forEach(numbers, 5, [](int x)
    {
        std::cout << x * x << ' ';
    });

    std::cout << '\n'; // 1 4 9 16 25

    return 0;
}
./InlineCallback
$ ./InlineCallback
1 4 9 16 25

Коли іменувати лямбду, а коли — ні? Якщо лямбда коротка і її сенс очевидний із контексту — передавайте прямо у виклик. Якщо лямбда складна або використовується кілька разів — збережіть в іменованій змінній:

// ✅ Коротка і зрозуміла — прямо у виклику
forEach(numbers, 5, [](int x) { std::cout << x << ' '; });

// ✅ Складна або використовується кілька разів — іменована
auto isPositiveAndEven = [](int x)
{
    return (x > 0) && (x % 2 == 0);
};

// Використовуємо isPositiveAndEven кілька разів...

Тип лямбди: чому auto?

У C++ кожна лямбда має унікальний, генерований компілятором тип, який ми не можемо написати вручну. Два однакові за виглядом лямбда-вирази мають різні типи. Саме тому для зберігання лямбди у змінній єдиний природний вибір — auto:

LambdaType.cpp
#include <iostream>

int main()
{
    auto lambdaA = [](int x) { return x + 1; };
    auto lambdaB = [](int x) { return x + 1; }; // ТОЙ САМИЙ ВИГЛЯД — але інший тип!

    // lambdaA = lambdaB; // ❌ Помилка: різні типи, хоч і однакова сигнатура

    std::cout << lambdaA(5) << '\n'; // 6
    std::cout << lambdaB(5) << '\n'; // 6

    return 0;
}
Насправді лямбда — це не функція. Компілятор перетворює її на об'єкт спеціального класу (функтор, functor), що містить operator(). Цим пояснюється унікальність типу: компілятор генерує унікальний клас для кожної лямбди.

Три способи зберігати лямбду

StoringLambda.cpp
#include <iostream>
#include <functional>

int main()
{
    // Спосіб 1: auto — зберігає реальний тип, без накладних витрат (рекомендовано)
    auto addA = [](double a, double b) { return a + b; };

    // Спосіб 2: std::function — гнучко, але є накладні витрати на виклик
    std::function<double(double, double)> addB = [](double a, double b) { return a + b; };

    // Спосіб 3: вказівник на функцію — лише якщо лямбда нічого не захоплює
    double (*addC)(double, double) = [](double a, double b) { return a + b; };

    std::cout << addA(1.5, 2.5) << '\n'; // 4
    std::cout << addB(3.0, 1.0) << '\n'; // 4
    std::cout << addC(2.0, 2.0) << '\n'; // 4

    return 0;
}

Коли що використовувати:

СитуаціяРекомендація
Локальна зміннаauto
Параметр функції (будь-який callable)std::function<R(Args...)> або шаблонний параметр
Лямбда без capture → параметр у C-APIвказівник на функцію
Лямбда, що захоплює зовнішні змінні (детально в статті 26), не може бути збережена у звичайному вказівнику на функцію. Для таких лямбд необхідно std::function або auto.

Передача лямбди у функцію через std::function

Коли функція приймає callable-параметр і ви хочете дозволити передавати як звичайні функції, так і лямбди — використовуйте std::function:

RepeatLambda.cpp
#include <iostream>
#include <functional>

// Функція, що викликає fn задану кількість разів
void repeat(int count, const std::function<void(int)>& fn)
{
    for (int i = 0; i < count; ++i)
        fn(i);
}

int main()
{
    // Передаємо лямбду прямо у виклик
    repeat(4, [](int i)
    {
        std::cout << "Крок " << i << '\n';
    });

    return 0;
}
./RepeatLambda
$ ./RepeatLambda
Крок 0
Крок 1
Крок 2
Крок 3

Узагальнені лямбди: параметри типу auto

Починаючи з C++14, параметри лямбди можуть мати тип auto. Така лямбда називається узагальненою (generic lambda) — вона автоматично адаптується до типу переданого аргументу:

GenericLambda.cpp
#include <iostream>

int main()
{
    // Узагальнена лямбда: auto-параметр — працює з будь-яким типом
    auto printValue = [](const auto& value)
    {
        std::cout << value << '\n';
    };

    printValue(42);       // виводить: 42       (auto деduced → int)
    printValue(3.14);     // виводить: 3.14     (auto deduced → double)
    printValue("Hello");  // виводить: Hello    (auto deduced → const char*)

    return 0;
}
./GenericLambda
$ ./GenericLambda
42
3.14
Hello

Важливий нюанс: для кожного унікального типу аргументу компілятор генерує окремий екземпляр лямбди. Це схоже на шаблони функцій. Саме тому у наступному прикладі callCount не є спільним між версіями для int і const char*:

StaticInGeneric.cpp
#include <iostream>

int main()
{
    auto trackCalls = [](auto value)
    {
        static int callCount = 0; // окремий для кожного типу!
        std::cout << callCount++ << ": " << value << '\n';
    };

    trackCalls("hello");  // 0: hello  (екземпляр для const char*)
    trackCalls("world");  // 1: world  (той самий екземпляр)
    trackCalls(10);       // 0: 10     (новий екземпляр для int!)
    trackCalls(20);       // 1: 20     (екземпляр для int)
    trackCalls("!");      // 2: !      (знову екземпляр для const char*)

    return 0;
}
./StaticInGeneric
$ ./StaticInGeneric
0: hello
1: world
0: 10
1: 20
2: !

Дві версії лямбди (const char* і int) мають окремі callCount — адже компілятор генерує два різні класи.


Явний тип повернення: trailing return type

Зазвичай компілятор виводить тип повернення лямбди автоматично з оператора return. Але якщо тіло лямбди містить кілька return з різними типами — це призведе до помилки:

ReturnTypeError.cpp
// ❌ Помилка: перший return повертає int, другий — double
auto divide = [](int x, int y, bool isInteger)
{
    if (isInteger)
        return x / y;                       // int
    else
        return static_cast<double>(x) / y;  // double ← несумісність!
};

Рішення — явно вказати тип повернення через trailing return type (->):

TrailingReturn.cpp
#include <iostream>

int main()
{
    // Явно вказуємо -> double: обидва return тепер конвертуються в double
    auto divide = [](int x, int y, bool isInteger) -> double
    {
        if (isInteger)
            return x / y;                       // int → неявна конвертація в double
        else
            return static_cast<double>(x) / y;  // double
    };

    std::cout << divide(7, 2, true)  << '\n'; // 3        (цілочисельний поділ)
    std::cout << divide(7, 2, false) << '\n'; // 3.5      (дійсний поділ)

    return 0;
}
./TrailingReturn
$ ./TrailingReturn
3
3.5

-> double після списку параметрів — це trailing return type, і він читається природно: «лямбда, що приймає int, int, bool і повертає double».

Деякі розробники завжди вказують trailing return type явно, навіть коли він очевидний, — для самодокументації. Це не є обов'язковим, але є гарною практикою для складніших лямбд.

Функтори Стандартної бібліотеки: коли лямбда не потрібна

Для найпоширеніших операцій — порівняння, арифметика, логіка — стандартна бібліотека надає готові функтори у заголовку <functional>. Замість написання власної лямбди для сортування за спаданням можна скористатися std::greater:

StdFunctors.cpp
#include <iostream>
#include <functional>
#include <algorithm>

int main()
{
    int numbers[] = { 13, 90, 5, 40, 80, 99 };
    const int SIZE = 6;

    // Без STL-алгоритмів — сортуємо вручну для демонстрації
    // std::sort(numbers, numbers + SIZE, std::greater<int>{}); // за спаданням

    // Еквівалент через лямбду:
    auto greaterThan = [](int a, int b) { return a > b; };

    // Сортування методом бульбашки для демонстрації (not std::sort)
    for (int i = 0; i < SIZE - 1; ++i)
    {
        for (int j = 0; j < SIZE - i - 1; ++j)
        {
            if (!greaterThan(numbers[j], numbers[j + 1]))
                std::swap(numbers[j], numbers[j + 1]);
        }
    }

    for (int i = 0; i < SIZE; ++i)
        std::cout << numbers[i] << ' ';
    std::cout << '\n';

    return 0;
}
./StdFunctors
$ ./StdFunctors
99 90 80 40 13 5

Зведення найкорисніших функторів STL:

ФункторЕквівалентна лямбдаОпис
std::greater<T>{}[](T a, T b){ return a > b; }a > b
std::less<T>{}[](T a, T b){ return a < b; }a < b
std::equal_to<T>{}[](T a, T b){ return a == b; }a == b
std::plus<T>{}[](T a, T b){ return a + b; }a + b
std::negate<T>{}[](T a){ return -a; }-a

Практичні приклади з детальним розбором

Теорія закладена — час закріпити її через конкретні, ретельно розібрані приклади. Кожен із них вирішує реальну задачу і демонструє окремий аспект лямбда-виразів. Читайте покрокові пояснення: вони навмисно виписані детально.


Приклад 1: countIf — підрахунок елементів за умовою

Задача: функція countIf приймає масив, його розмір і предикат-лямбду. Вона повертає кількість елементів, для яких предикат повертає true. Це прямий аналог std::count_if з STL.

CountIf.cpp
#include <iostream>
#include <functional>

// Приймає будь-який callable bool(int) через std::function
int countIf(int* array, int size, const std::function<bool(int)>& predicate)
{
    int count = 0;

    for (int i = 0; i < size; ++i)
    {
        if (predicate(array[i])) // виклик лямбди — такий самий, як виклик функції
            ++count;
    }

    return count;
}

int main()
{
    int numbers[] = { -4, 7, 0, 12, -1, 3, 8, -9, 5, 6 };
    const int SIZE = 10;

    // Лямбда 1: парні числа
    auto isEven = [](int n)
    {
        return n % 2 == 0;
    };

    // Лямбда 2: від'ємні числа
    auto isNegative = [](int n)
    {
        return n < 0;
    };

    // Лямбда 3: у діапазоні [1, 9]
    auto isInRange = [](int n)
    {
        return n >= 1 && n <= 9;
    };

    std::cout << "Парних:       " << countIf(numbers, SIZE, isEven)    << '\n';
    std::cout << "Від'ємних:    " << countIf(numbers, SIZE, isNegative) << '\n';
    std::cout << "В діапазоні:  " << countIf(numbers, SIZE, isInRange)  << '\n';

    // Можна передати лямбду прямо у виклик:
    std::cout << "Нулів:        " << countIf(numbers, SIZE, [](int n) { return n == 0; }) << '\n';

    return 0;
}
./CountIf
$ ./CountIf
Парних: 5
Від'ємних: 3
В діапазоні: 4
Нулів: 1

Рядок 5. const std::function<bool(int)>& predicate — параметр типу «константне посилання на std::function, що приймає int і повертає bool``. Константне посилання (const&) тут є оптимізацією: std::function— важкий об'єкт, передавати його за значенням означало б зайве копіювання. Але нам читати непотрібно змінювати предикат — томуconst`.

Рядок 11. predicate(array[i]) — лямбда викликається так само, як звичайна функція. std::function «обгортає» лямбду і надає однаковий синтаксис виклику незалежно від реального типу callable.

Рядок 26–29. Лямбда isNegative — однорядкове тіло. Задля читабельності її можна було б написати в один рядок: auto isNegative = [](int n) { return n < 0; };. Обидва записи рівнозначні.

Рядок 47. [](int n) { return n == 0; } — лямбда прямо у виклику, без іменування. Виправдано: умова «одно рівне нулю» є простою і зрозумілою без назви.


Приклад 2: transformArray — перетворення елементів

Задача: функція transformArray замінює кожен елемент масиву результатом застосування лямбди-трансформера. Операція — «in-place» (без виділення нового масиву).

TransformArray.cpp
#include <iostream>
#include <functional>

// Замінює кожен елемент результатом fn(element). In-place.
void transformArray(int* array, int size, const std::function<int(int)>& fn)
{
    for (int i = 0; i < size; ++i)
        array[i] = fn(array[i]); // Читаємо старе значення → передаємо в fn → записуємо результат
}

void printArray(int* array, int size)
{
    for (int i = 0; i < size; ++i)
        std::cout << array[i] << ' ';
    std::cout << '\n';
}

int main()
{
    int numbers[] = { 1, -2, 3, -4, 5, -6 };
    const int SIZE = 6;

    std::cout << "До:        ";
    printArray(numbers, SIZE);

    // Трансформація 1: подвоїти кожен елемент
    transformArray(numbers, SIZE, [](int x)
    {
        return x * 2;
    });

    std::cout << "Подвоїти:  ";
    printArray(numbers, SIZE);

    // Трансформація 2: взяти абсолютне значення
    // abs(x): якщо x < 0 → -x, інакше → x
    transformArray(numbers, SIZE, [](int x)
    {
        return x < 0 ? -x : x;
    });

    std::cout << "Abs:       ";
    printArray(numbers, SIZE);

    // Трансформація 3: звести в квадрат
    transformArray(numbers, SIZE, [](int x)
    {
        return x * x;
    });

    std::cout << "Квадрат:   ";
    printArray(numbers, SIZE);

    return 0;
}
./TransformArray
$ ./TransformArray
До: 1 -2 3 -4 5 -6
Подвоїти: 2 -4 6 -8 10 -12
Abs: 2 4 6 8 10 12
Квадрат: 4 16 36 64 100 144

Рядок 8. array[i] = fn(array[i]) — ця єдина, компактна рядок і є серцем transformArray. Зліва — запис за індексом i, справа — читання того ж елемента і передача його у fn. Порядок операцій: спочатку fn(array[i]) виконується до кінця і обчислюється нове значення, потім воно присвоюється в array[i]. Старе значення при цьому вже не потрібне — і це безпечно.

Рядок 38–41. x < 0 ? -x : x — тернарний оператор замість if. Для лямбди з єдиним виразом return це доречно: весь вираз читається як математична формула |x|.

Архітектурна ідея: зверніть, що після transformArray(numbers, SIZE, [](int x) { return x * 2; }) масив назавжди змінюється. Наступний виклик Abs вже бачить подвоєні значення — і це навмисне: ми демонструємо ланцюгове застосування трансформацій до одного масиву. В реальних програмах часто замість in-place трансформації копіюють масив, щоб зберегти оригінал.


Приклад 3: сортування бульбашкою з компаратором-лямбдою

Задача: реалізувати сортування бульбашкою (bubble sort), де критерій порівняння — лямбда. Це демонструє, що лямбда замінює іменований callback так само ефективно, але компактніше.

BubbleSortLambda.cpp
#include <iostream>
#include <functional>
#include <utility> // для std::swap

void bubbleSort(int* array, int size, const std::function<bool(int, int)>& shouldSwap)
{
    for (int pass = 0; pass < size - 1; ++pass)
    {
        for (int i = 0; i < size - pass - 1; ++i)
        {
            // Запитуємо лямбду: чи треба міняти місцями array[i] і array[i+1]?
            if (shouldSwap(array[i], array[i + 1]))
                std::swap(array[i], array[i + 1]);
        }
    }
}

void printArray(int* array, int size)
{
    for (int i = 0; i < size; ++i)
        std::cout << array[i] << ' ';
    std::cout << '\n';
}

int main()
{
    int numbersA[] = { 64, 34, 25, 12, 22, 11, 90 };
    int numbersB[] = { 64, 34, 25, 12, 22, 11, 90 }; // копія для другого сортування
    const int SIZE = 7;

    // Лямбда 1: за зростанням — міняти, якщо лівий БІЛЬШИЙ за правий
    bubbleSort(numbersA, SIZE, [](int a, int b)
    {
        return a > b; // a зліва, b справа; якщо a > b — вони в неправильному порядку
    });

    std::cout << "За зростанням: ";
    printArray(numbersA, SIZE);

    // Лямбда 2: за спаданням — міняти, якщо лівий МЕНШИЙ за правий
    bubbleSort(numbersB, SIZE, [](int a, int b)
    {
        return a < b; // якщо a < b — для спадного порядку вони стоять неправильно
    });

    std::cout << "За спаданням:  ";
    printArray(numbersB, SIZE);

    return 0;
}
./BubbleSortLambda
$ ./BubbleSortLambda
За зростанням: 11 12 22 25 34 64 90
За спаданням: 90 64 34 25 22 12 11

Рядок 5. const std::function<bool(int, int)>& shouldSwap — зверніть на ім'я параметра. Замість безликого compare або fn ми використовуємо shouldSwap — ім'я, що точно описує семантику: «чи треба міняти місцями?». Хороше ім'я callback-параметра є частиною документації функції.

Рядок 12. if (shouldSwap(array[i], array[i + 1])) — цей рядок нічого не знає про конкретну умову. bubbleSort повністю відокремлена від логіки сортування. Зверніть: перший аргумент — лівий (поточний) елемент, другий — правий (наступний). Саме тому:

  • Лямбда a > b означає «лівий більший → він має стояти правіше → міняємо» → зростання.
  • Лямбда a < b означає «лівий менший → він має стояти правіше → міняємо» → спадання.

Рядки 28–29. numbersA[] і numbersB[] — два окремі масиви з однаковими значеннями. Ми вже знаємо з попередніх статей: якщо передати один і той самий масив двічі, перше сортування змінить його, і друге сортуватиме вже змінений набір — тести були б невалідними.


Приклад 4: знаходження мінімуму і максимуму через узагальнену лямбду

Задача: написати функцію findExtreme, що знаходить «крайній» елемент масиву — мінімум чи максимум, залежно від переданої лямбди-порівнювача.

FindExtreme.cpp
#include <iostream>
#include <functional>

// Знаходить "крайній" елемент масиву згідно з критерієм isBetter.
// isBetter(candidate, current) → true, якщо candidate є "кращим" за current
int findExtreme(int* array, int size, const std::function<bool(int, int)>& isBetter)
{
    int extreme = array[0]; // починаємо з першого елемента як поточного "чемпіона"

    for (int i = 1; i < size; ++i) // перебираємо решту, починаючи з другого
    {
        // Якщо поточний елемент "кращий" за досі знайдений — оновлюємо
        if (isBetter(array[i], extreme))
            extreme = array[i];
    }

    return extreme;
}

int main()
{
    int numbers[] = { 3, -7, 14, 0, -2, 11, 8 };
    const int SIZE = 7;

    // Лямбда для мінімуму: candidate є "кращим", якщо він менший
    auto isSmaller = [](int candidate, int current)
    {
        return candidate < current;
    };

    // Лямбда для максимуму: candidate є "кращим", якщо він більший
    auto isLarger = [](int candidate, int current)
    {
        return candidate > current;
    };

    // Лямбда для "найближчого до нуля":
    // candidate є "кращим", якщо його абсолютне значення менше
    auto isCloserToZero = [](int candidate, int current)
    {
        int absCand = candidate < 0 ? -candidate : candidate;
        int absCurr = current < 0  ? -current  : current;
        return absCand < absCurr;
    };

    std::cout << "Мінімум:          " << findExtreme(numbers, SIZE, isSmaller)      << '\n';
    std::cout << "Максимум:         " << findExtreme(numbers, SIZE, isLarger)        << '\n';
    std::cout << "Ближче до нуля:   " << findExtreme(numbers, SIZE, isCloserToZero) << '\n';

    return 0;
}
./FindExtreme
$ ./FindExtreme
Мінімум: -7
Максимум: 14
Ближче до нуля: 0

Концепція «чемпіона» (рядок 8). int extreme = array[0] — ми починаємо з першого елемента і умовно вважаємо його «поки що найкращим». Потім у циклі кожен наступний елемент порівнюється з поточним «чемпіоном». Якщо новий елемент «кращий» (за критерієм isBetter) — він стає новим чемпіоном. Цей патерн є стандартним для всіх алгоритмів «знайти крайній».

Рядок 13. isBetter(array[i], extreme) — перший аргумент завжди поточний кандидат (перевіряємо), другий — поточний чемпіон (з яким порівнюємо). Це конвенція: лямбда-порівнювач відповідає на питання «чи є перший аргумент кращим за другий?»

Рядки 38–42. Лямбда isCloserToZero є найцікавішою: вона обчислює абсолютне значення двох чисел вручну (через тернарний оператор) і порівнює їх. Зверніть — це кілька рядків тіла лямбди, і це абсолютно нормально. Лямбда — це повноцінна функція, і вона може містити будь-яку кількість рядків.


Приклад 5: лямбда з кількома рядками — валідатор введення

Задача: написати функцію валідації числа, що перевіряє одразу кілька умов. Це демонструє, що лямбда — не завжди «один рядок».

Validator.cpp
#include <iostream>
#include <functional>

// Перевіряє число за набором правил. Якщо хоч одне порушене — повертає false.
bool validate(int value, const std::function<bool(int)>* rules, int ruleCount)
{
    for (int i = 0; i < ruleCount; ++i)
    {
        if (!rules[i](value)) // якщо i-те правило повернуло false
        {
            std::cout << "  Правило #" << i + 1 << " порушено!\n";
            return false;     // зупиняємось при першому порушенні
        }
    }

    return true; // усі правила дотримані
}

int main()
{
    // Масив правил-лямбд — кожна перевіряє одну умову
    std::function<bool(int)> rules[] =
    {
        // Правило 1: не від'ємне
        [](int n)
        {
            return n >= 0;
        },

        // Правило 2: менше 100
        [](int n)
        {
            return n < 100;
        },

        // Правило 3: парне
        [](int n)
        {
            return n % 2 == 0;
        },

        // Правило 4: не кратне 7
        [](int n)
        {
            return n % 7 != 0;
        }
    };

    const int RULE_COUNT = 4;

    int testValues[] = { 42, -5, 56, 100, 14 };

    for (int i = 0; i < 5; ++i)
    {
        std::cout << "Перевірка " << testValues[i] << ":\n";

        if (validate(testValues[i], rules, RULE_COUNT))
            std::cout << "  ✅ Валідне\n";
        else
            std::cout << "  ❌ Невалідне\n";
    }

    return 0;
}
./Validator
$ ./Validator
Перевірка 42:
✅ Валідне
Перевірка -5:
Правило #1 порушено!
❌ Невалідне
Перевірка 56:
Правило #4 порушено!
❌ Невалідне
Перевірка 100:
Правило #2 порушено!
❌ Невалідне
Перевірка 14:
Правило #4 порушено!
❌ Невалідне

Рядки 21–46. std::function<bool(int)> rules[]масив лямбд. Кожен елемент масиву є окремою функцією зі своєю логікою. Ініціалізуємо масив як звичайний масив, лише замість цілих чисел — лямбди. Зверніть: для масиву лямбд потрібен std::function, а не auto, бо всі елементи масиву мають бути одного типу, а auto генерує різні типи для різних лямбд.

Рядок 9. if (!rules[i](value))rules[i] звертається до i-го елемента масиву (який є std::function), (value) викликає його з аргументом. ! інвертує результат: якщо правило каже «некоректно» (false), ми входимо у тіло if.

Рядок 12. return false після першого порушеного правила — «швидка відмова» (fail fast). Немає сенсу перевіряти решту правил, якщо вже порушено перше. Це економить час.

Ключова ідея. Масив лямбд rules[] — це декларативний спосіб задати набір умов. Замість великого if (n >= 0 && n < 100 && n % 2 == 0 && n % 7 != 0) ми маємо чотири окремі, іменовані-коментарями, перевірки, кожну з яких легко додати, видалити або замінити незалежно від інших.


Практика та підсумок

Практичні завдання

Рівень 1 — Базовий

Завдання 1. Напишіть лямбду square, що зводить число в квадрат. Збережіть її у auto-змінній. Викличте з аргументами 3, 5, 7 і виведіть результати.

Завдання 2. Напишіть лямбду isPositive, що повертає true, якщо число більше нуля. Використайте її у функції forEach (або власному циклі) для виводу лише позитивних чисел з масиву {-3, 1, -1, 4, 0, 7, -2}.

Завдання 3. Чому наступний код не компілюється? Виправте його двома способами:

auto classify = [](int x) {
    if (x >= 0)
        return x;          // int
    else
        return -1.0 * x;   // double — конфлікт!
};

Рівень 2 — Логіка

Завдання 4. Напишіть функцію int countIf(int* array, int size, std::function<bool(int)> predicate), що підраховує кількість елементів, для яких predicate повертає true. Протестуйте з лямбдами:

  • «є парним»
  • «більше 10»
  • «ділиться на 3»

Завдання 5. Реалізуйте функцію void transform(int* array, int size, std::function<int(int)> fn), що замінює кожен елемент результатом fn(element). Протестуйте з лямбдами:

  • «подвоїти»
  • «взяти абсолютне значення» (від'ємне → позитивне)

Завдання 6. Напишіть узагальнену лямбду printPair, що приймає два const auto& параметри і виводить їх у форматі "[a, b]". Протестуйте з парами: (int, int), (int, const char*), (double, double).

Рівень 3 — Архітектура

Завдання 7. Реалізуйте систему «конвеєра трансформацій» через масив std::function:

std::function<int(int)> pipeline[] = {
    [](int x) { return x * 2; },   // подвоїти
    [](int x) { return x + 10; },  // додати 10
    [](int x) { return x * x; }    // звести в квадрат
};

Напишіть функцію int applyPipeline(int value, std::function<int(int)>* steps, int count), що послідовно застосовує кожен крок. Протестуйте для числа 3: 3 → 6 → 16 → 256.


Підсумок

Синтаксис

[capture](params) -> Type { body }. Capture і params можуть бути порожніми. -> Type опціональний, якщо тип виводиться однозначно.

Тип і зберігання

Кожна лямбда — унікальний тип. Зберігайте через auto (локально) або std::function<R(Args...)> (як параметр функції).

auto-параметри (C++14)

[](const auto& a, const auto& b) — узагальнена лямбда. Компілятор генерує окремий екземпляр для кожного типу.

Коли не лямбда

Складна логіка, що потребує тестування → іменована функція. Проста операція (порівняння, арифметика) → функтор STL std::greater, std::less тощо.

У наступній статті ми розглянемо захоплення лямбди (lambda captures) — механізм, що дозволяє лямбді «бачити» змінні з навколишнього контексту, і саме тут лямбди демонструють всю свою справжню потужність.