Лямбда‑функции в C++: захваты, mutable, возвращаемые типы и лучшие практики

Лямбда‑функции в C++: захваты, mutable, возвращаемые типы и лучшие практики

Лямбда‑функции в C++: захваты, mutable, возвращаемые типы и лучшие практики

Запрос, под который оптимизирована статья: лямбда‑функции в C++. Если вы хотите быстро и безопасно писать компактные функции прямо в месте вызова — лямбды незаменимы. Разберёмся на практических примерах, без лишней теории.

Базовый синтаксис лямбда‑функций

Общий вид: [](параметры) спецификаторы -> возвращаемый_тип { тело; }. Минимальная лямбда:

#include <iostream>

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

Захваты: по значению, по ссылке и смешанные

Захват управляет тем, какие внешние переменные доступны внутри лямбды.

  • [] — ничего не захватываем
  • [=] — по значению (копии) всех используемых переменных
  • [&] — по ссылке всех используемых переменных
  • [x, &y] — явный смешанный захват
  • [this] — захват указателя this (копий нет)
  • [*this] (C++23) — копия объекта, на который указывает this
  • #include <algorithm>
    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> v{5, 1, 4, 2, 3};
        int pivot = 3;
    
        // Сортируем так, чтобы числа < pivot были первыми
        std::sort(v.begin(), v.end(), [=](int a, int b) {
            auto ka = (a < pivot);
            auto kb = (b < pivot);
            if (ka != kb) return ka > kb; // < перед > — логика группировки
            return a < b;
        });
    
        for (int x : v) std::cout << x << ' ';
    }
    

    Совет: по умолчанию используйте явный захват (например, [pivot]). Глобальные [=] и [&] удобны, но повышают риск случайно захватить лишнее и усложнить сопровождение.

    Инициализирующий и перемещающий захват (C++14+)

    Можно создавать новые переменные прямо в списке захвата — удобно для перемещения уникальных ресурсов или подготовки состояния.

    #include <memory>
    #include <thread>
    #include <iostream>
    
    int main() {
        auto p = std::make_unique<int>(42);
        std::thread t([q = std::move(p)]() {
            // q - единственный владелец внутри лямбды
            std::cout << *q << "n";
        });
        t.join();
        // p теперь пуст (nullptr)
    }
    

    Такой приём безопасен в многопоточных сценариях: вы явно передаёте владение в лямбду.

    mutable: изменение копий, а не внешних значений

    Лямбды по умолчанию «константные»: захваченные по значению переменные менять нельзя. mutable снимает это ограничение, но меняет только копии.

    #include <iostream>
    
    int main() {
        int n = 0;
        auto inc_copy = [n]() mutable {
            n++;              // ок, меняем копию
            return n;
        };
        std::cout << inc_copy() << ", " << inc_copy() << "n"; // 1, 2
        std::cout << n << "n"; // исходный n по-прежнему 0
    }
    

    Чтобы менять внешнюю переменную, захватывайте по ссылке: [&n]{ n++; }.

    Возвращаемые типы: auto и явное указание

    Компилятор выводит тип результата автоматически, но если в ветках возвращаются разные типы, нужна явная аннотация.

    auto f = [](bool flag) {
        if (flag) return 1;      // int
        // return 1.5;           // ошибка: разные типы
        return 2;                // ок, везде int
    };
    
    auto g = [](bool flag) -> double {
        if (flag) return 1;      // 1.0
        return 1.5;              // 1.5
    };
    

    Generic‑лямбды (C++14+): параметры как auto

    Идеально подходят для алгоритмов стандартной библиотеки и универсальных коллбеков без явного шаблонного синтаксиса.

    #include <algorithm>
    #include <cctype>
    #include <string>
    #include <vector>
    
    int main() {
        std::vector<std::string> words{"C++", "Lambda", "Rocks"};
        std::transform(words.begin(), words.end(), words.begin(), [](auto s) {
            for (auto& ch : s) ch = std::toupper(static_cast(ch));
            return s;
        });
    }
    

    Лямбды в алгоритмах: фильтрация и подсчёт

    #include <algorithm>
    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> v{1,2,3,4,5,6};
    
        v.erase(std::remove_if(v.begin(), v.end(), [](int x){ return x % 2 == 0; }), v.end());
        // Остались только нечётные: 1 3 5
        for (int x : v) std::cout << x << ' ';
    }
    

    std::function и лямбды: когда это оправдано

    std::function — тип-обёртка для хранение вызываемого объекта с типовым стиранием. Это гибко, но медленнее и требует выделений. Рекомендации:

  • Внутри шаблонов и локально — храните лямбду в auto или передавайте как универсальный параметр (perfect forwarding).
  • На границах API (коллбеки, полиморфные контейнеры) — используйте std::function<...> для стабильного интерфейса.
  • #include <functional>
    #include <iostream>
    
    void run(std::function<void(int)> cb) { cb(42); }
    
    int main() {
        auto fast = [](int x){ std::cout << x << "n"; };
        run(fast); // удобно на границе API
    }
    

    Статeless‑лямбда как указатель на функцию

    Если лямбда ничего не захватывает, её можно неявно преобразовать к указателю на функцию подходящей сигнатуры — удобно для старых C‑API.

    void (*fp)(int) = [](int x){ /* ... */ };
    fp(10);
    

    Типичные ошибки и как их избежать

  • Дэнглинг‑ссылки: вы захватили ссылкой переменную, которая вышла из области видимости. Правило — для асинхронных задач используйте захват по значению или перемещающий захват.
  • Глобальные [=] или [&]: скрытые зависимости. Предпочитайте явный захват.
  • Тяжёлые копии: большие объекты лучше захватывать по ссылке или по перемещению.
  • Сложные типы возвращаемых значений: при неоднородных ветках укажите -> тип явно.
  • Небольшой чек‑лист по лямбдам

  • Пишите очевидный список захвата: [x], [&y], [z = make()].
  • Используйте mutable, когда меняете копию захваченного значения внутри лямбды.
  • Предпочитайте auto для хранения лямбды, а std::function — для публичных API.
  • Для многопоточности — перемещающий захват ресурсов ([p = std::move(p)]).
  • Статeless‑лямбды легко передавать туда, где ожидают указатель на функцию.
  • Куда двигаться дальше

    Потренируйтесь переписать свои sort/filter/remove_if на лямбды, попробуйте перемещающий захват и generic‑лямбды. А чтобы системно закрыть пробелы по современному C++ (включая лямбды, ООП, STL и практику), посмотрите программу и первые уроки здесь: C++ с Нуля до Гуру — посмотреть программу и начать бесплатно.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий