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

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

C++ SELF
22 уровень , 0 лекция
Открыта

1. Что такое лямбда и как она устроена

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

Лямбда позволяет создать вызываемое значение прямо в выражении. То есть вы не «объявляете функцию где-то выше», а создаёте объект, который можно вызвать как функцию. Это сильно помогает, когда правило должно быть рядом с использованием: сортируем «по приоритету, а если равны — по названию», фильтруем «все выполненные», проверяем «есть ли хоть одна срочная задача».

Лямбда — выражение, которое создаёт значение

Важно начать с правильной mental model (модели в голове). Лямбда — это не «магический синтаксис объявления функции». Лямбда — это выражение, как 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["(params)"]
    B --> C["-> return_type (optional)"]
    C --> D["{ body }"]

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

Начать лучше с самого простого случая: лямбда без параметров и без возвращаемого значения. Это буквально «мини-функция на месте». Такой пример кажется игрушечным, но он отлично тренирует чтение синтаксиса и понимание «создал — потом вызвал».

#include <iostream>

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

    ping();
    ping();
}

Здесь полезно заметить две вещи. Во-первых, ping — обычная переменная, её можно передавать и хранить (про «как хранить» подробно будет отдельная лекция). Во-вторых, лямбда ничего не делает «сама по себе»: пока вы не написали ping();, она не выполняется.

3. Параметры и возвращаемое значение

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

Теперь добавим параметры. На этом этапе лямбда почти не отличается от обычной функции: в круглых скобках перечисляются параметры с типами. С точки зрения синтаксиса это удобно: вы уже умеете писать функции, значит половина навыка уже есть.

Представим наш учебный консольный проект (пусть это будет маленький менеджер задач 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). В таких ситуациях проще и честнее зафиксировать один тип результата и под него подогнать все ветки.

4. «Создать и сразу вызвать» и пример из 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.

5. Типичные ошибки в синтаксисе лямбды

Ошибка №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: путаница «квадратные скобки — это массив?»
Очень частая психологическая ловушка: [] напоминает индексацию. В лямбдах это не индексатор и не массив, а список захвата. Сегодня он пустой, поэтому [] выглядит особенно странно. Просто запомните: в лямбдах квадратные скобки — это «про внешние переменные», а не «про доступ по индексу». Подробно и с примерами мы разберём это в следующей лекции.

1
Задача
C++ SELF, 22 уровень, 0 лекция
Недоступна
Приветствие на стойке
Приветствие на стойке
1
Задача
C++ SELF, 22 уровень, 0 лекция
Недоступна
Два режима расчёта
Два режима расчёта
1
Задача
C++ SELF, 22 уровень, 0 лекция
Недоступна
Процент готовности
Процент готовности
1
Задача
C++ SELF, 22 уровень, 0 лекция
Недоступна
Цена по промокоду
Цена по промокоду
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ