JavaRush /Курси /C++ SELF /Синтаксис лямбди в C++: [](…) { … }

Синтаксис лямбди в C++: [](…) { … }

C++ SELF
Рівень 22 , Лекція 0
Відкрита

1. Що таке лямбда і як вона влаштована

Коли ви вперше бачите лямбду, часто виникає відчуття: «А це точно C++? Може, я випадково відкрив JavaScript?». Насправді ідея проста: інколи вам потрібне невелике правило на 1–3 рядки, яке використовується рівно в одному місці. Створювати заради нього окрему функцію — це як заводити окремий холодильник заради одного пельменя: можна, але дивно.

Лямбда дозволяє створити обʼєкт, який можна викликати, прямо у виразі. Тобто ви не «оголошуєте функцію десь вище», а створюєте обʼєкт, що поводиться як функція. Це дуже допомагає, коли правило має бути поруч із місцем використання: сортуємо «за пріоритетом, а якщо однакові — за назвою», фільтруємо «усі виконані», перевіряємо «чи є хоч одне термінове завдання».

Лямбда — це вираз, який створює значення

Важливо одразу сформувати правильне уявлення. Лямбда — це не «магічний синтаксис оголошення функції». Лямбда — це вираз, як 1 + 2 або std::string{"hi"}. Вона створює значення (обʼєкт), яке можна зберегти у змінній, кудись передати або одразу викликати.

Дивіться на лямбду як на «коробку з кнопкою»: ви її створили — і вона просто є. Натиснули кнопку — вона спрацювала.

#include <iostream>

int main() {
    auto hello = [] {
        std::cout << "Hello!\n"; // Hello!
    };

    hello(); // виклик «коробки з кнопкою»
}

Зверніть увагу на важливу дрібницю: після auto hello = [] { ... }; стоїть ;, тому що це звичайне присвоєння змінній. Це одна з найпоширеніших дрібних помилок серед початківців.

Анатомія запису [] (params) -> R { body }

Щоб лямбди перестали виглядати як «набір дужок, що налякав компілятор», корисно розібрати запис на частини. Повна форма лямбди виглядає так: [] (параметри) -> тип { тіло }. Деякі частини можна опускати, і саме тому спочатку здається, що «варіантів надто багато».

Домовмося: у цій лекції ми свідомо не використовуємо захоплення (тобто в [] у нас буде порожньо). Захоплення — це тема наступної лекції, і там буде чимало цікавих граблів.

Частина Приклад Сенс
Список захоплення
[]
Доступ до зовнішніх змінних (сьогодні залишаємо порожнім)
Параметри
(int a, int b)
Як у звичайної функції
Явний тип результату
-> double
Необовʼязково: потрібен, коли треба зафіксувати тип
Тіло
{ return a + b; }
Команди, як у звичайній функції

Невелика «схема-підказка»:

flowchart LR
    A["[]"] --> B["(параметри)"]
    B --> C["-> тип_результату (необовʼязково)"]
    C --> D["{ тіло }"]

Мінімальна лямбда: [] { ... }

Почати найкраще з найпростішого випадку: лямбда без параметрів і без значення, яке повертається. Це буквально «мініфункція на місці». Такий приклад здається іграшковим, але він чудово тренує читання синтаксису й розуміння схеми «створив — потім викликав».

#include <iostream>

int main() {
    auto ping = [] {
        std::cout << "ping\n"; // ping
    };

    ping();
    ping();
}

Тут корисно помітити дві речі. По-перше, ping — це звичайна змінна, її можна передавати й зберігати (про те, як саме це робити, детально буде в окремій лекції). По-друге, лямбда нічого не робить «сама собою»: поки ви не написали ping();, вона не виконується.

2. Параметри та значення, яке повертається

Параметри лямбди: як у функції, тільки поруч

Тепер додамо параметри. На цьому етапі лямбда майже не відрізняється від звичайної функції: у круглих дужках ви перелічуєте параметри з типами. Із погляду синтаксису це зручно: ви вже вмієте писати функції, отже половина справи вже зроблена.

Уявімо наш навчальний консольний проєкт — нехай це буде невеликий менеджер завдань TaskBox. Кожне завдання має пріоритет, а ми хочемо швидко обчислювати «штраф» за низький пріоритет.

#include <iostream>

int main() {
    auto penalty = [](int priority) {
        return (10 - priority) * 5;
    };

    std::cout << penalty(8) << '\n'; // 10
    std::cout << penalty(2) << '\n'; // 40
}

Параметрів може бути кілька, як і у звичайній функції:

#include <iostream>

int main() {
    auto sum = [](int a, int b) {
        return a + b;
    };

    std::cout << sum(2, 3) << '\n'; // 5
}

Сюди ж належить і використання посилань та const, які ви вже знаєте. Наприклад, якщо ви передаєте рядок, зазвичай вигідніше передати const std::string&, щоб не копіювати його:

#include <iostream>
#include <string>

int main() {
    auto starts_with_hash = [](const std::string& s) {
        return !s.empty() && s[0] == '#';
    };

    std::cout << starts_with_hash("#todo") << '\n'; // 1
    std::cout << starts_with_hash("todo") << '\n';  // 0
}

Повернення значення та визначення типу результату

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

Найчастіший сценарій: повернути число, рядок або bool:

#include <iostream>

int main() {
    auto is_even = [](int x) {
        return x % 2 == 0;
    };

    std::cout << is_even(10) << '\n'; // 1
    std::cout << is_even(7) << '\n';  // 0
}

Якщо лямбда «нічого не повертає» (тобто повертає void), ви просто не пишете return зі значенням:

#include <iostream>

int main() {
    auto print_line = [](int x) {
        std::cout << "value = " << x << '\n'; // value = 42
    };

    print_line(42);
}

Є важливе правило, яке краще запамʼятати вже зараз: якщо в тілі лямбди є return, то всі return мають повертати один і той самий тип за змістом. Компілятор не любить, коли в одній гілці ви повертаєте int, а в іншій — double або std::string. Іноді це можна виправити явним типом результату — і це якраз наступна тема.

Явний тип результату: -> T

Іноді лямбда за замовчуванням повертає не те, що ви хотіли. Найтиповіший приклад — ділення цілих чисел. Якщо ви ділите int на int, вийде int, і дробова частина відкинеться. Це не помилка компілятора — так працює арифметика.

Щоб явно сказати: «я хочу double», можна написати -> double. Це читається як «стрілочка типу результату».

#include <iostream>

int main() {
    auto div_real = [](int a, int b) -> double {
        return static_cast<double>(a) / b;
    };

    std::cout << div_real(5, 2) << '\n'; // 2.5
}

Чому тут корисно одразу використати два прийоми (-> double і static_cast<double>)?

-> double — це контракт: «результат лямбди — дійсне число».
static_cast<double>(a) — це «підготовка» обчислення, щоб і ділення було дійсним, а не цілочисельним.

Є й інший типовий сценарій: у різних гілках ви повертаєте значення, які «схожі», але насправді мають різні типи. Наприклад, 0 (це int) і 0.0 (це double). У таких ситуаціях простіше й чесніше зафіксувати один тип результату й узгодити з ним усі гілки.

3. «Створити й одразу викликати» та приклад із TaskBox

«Створити й одразу викликати»: лямбда на один раз

Іноді вам потрібне обчислення просто тут і зараз, але ви не хочете створювати ні окрему функцію, ні навіть окрему змінну. У C++ можна створити лямбду й одразу її викликати: після фігурних дужок просто пишете (аргументи).

Цей прийом схожий на одноразову річ: скористалися — й пішли далі. Головне — не зловживати ним, щоб код не перетворився на ребус.

#include <iostream>

int main() {
    int x = 6;
    int y = 9;

    int mx = [](int a, int b) {
        return (a > b) ? a : b;
    }(x, y);

    std::cout << mx << '\n'; // 9
}

Зверніть увагу на важливий момент: ми не використали захоплення. Ми передали x і y як параметри, тобто лямбда залишилася «чистою» й незалежною від зовнішніх змінних. Це добра звичка: доки можна, передавайте значення через параметри. Захоплення — потужний інструмент, але ним легко скористатися так, що потім доведеться розбиратися (і так, про це буде в наступній лекції).

Приклад із TaskBox: правило поруч із місцем застосування

Щоб лямбди не здавалися «математикою заради математики», розгляньмо невеликий фрагмент, який органічно вписується в менеджер завдань. Припустімо, у нас є модель завдання, і ми хочемо визначити, чи важливіше завдання A за завдання B. Сьогодні ми не робитимемо сортування — це тема окремої лекції, — але можемо написати правило порівняння й перевірити його простим викликом.

#include <iostream>
#include <string>

struct Task {
    std::string title;
    int priority{};
};

int main() {
    auto more_important = [](const Task& a, const Task& b) {
        return a.priority > b.priority;
    };

    Task t1{"Buy milk", 2};
    Task t2{"Pay rent", 9};

    std::cout << more_important(t2, t1) << '\n'; // 1
}

Ключова ідея: лямбда живе поруч із тим кодом, де вона потрібна, і читач одразу бачить, за яким правилом порівнюємо. А ще вона не змушує вигадувати імʼя глобальної функції на кшталт isTaskMoreImportant_v2_final_real.

4. Типові помилки в синтаксисі лямбди

Помилка № 1: забули крапку з комою після присвоєння лямбди змінній.
Конструкція auto f = [] { ... } — це звичайне оголошення змінної з ініціалізацією. Тому ; обовʼязкова. Компілятор зазвичай скаржиться чимось на кшталт «expected ‘;’», і це один із тих випадків, коли він має рацію, а ви просто втомилися.

Помилка № 2: очікування, що лямбда виконається одразу після створення.
Лямбда — це не «магія автоматичного запуску». Вона створює обʼєкт. Виконання починається лише під час виклику f() або за сценарієм «створити й одразу викликати» [] { ... }(). Якщо ви створили auto log = [] { ... }; і нічого не сталося — так і задумано.

Помилка № 3: випадково отримали цілочисельне ділення.
Запис return a / b;, якщо int a, int b, повертає int. Якщо ви хотіли 2.5, а отримали 2, це не «арифметика з дійсними числами зламалася», а просто ви не сказали компілятору, що хочете дійсну арифметику. Лікується static_cast<double>(a) і, за потреби, явним -> double.

Помилка № 4: різні типи в різних return усередині однієї лямбди.
Якщо в одній гілці ви повертаєте 0, а в іншій 0.5, або в одній гілці рядок, а в іншій число — компілятор не зобовʼязаний вгадувати, що ви «мали на увазі». У простих випадках допомагає привести значення, які повертаються, до одного типу або зафіксувати його через -> T, але частіше правильніше спростити логіку, щоб вона була простішою й очевиднішою.

Помилка № 5: плутанина «квадратні дужки — це масив?»
Дуже поширена психологічна пастка: [] нагадує індексацію. У лямбдах це не індексація і не масив, а список захоплення. Сьогодні він порожній, тому [] виглядає особливо дивно. Просто запамʼятайте: у лямбдах квадратні дужки стосуються зовнішніх змінних, а не доступу за індексом. Детально й на прикладах ми розберемо це в наступній лекції.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ