
Привет, Хабр!
Сегодня рассмотрим лямбда-выражения в C++ и их эволюцию с момента появления в стандарте C++11 и до последних обновлений в C++20.
Лямбда-выражения в C++ — это анонимные функции, которые позволяют писать инлайн-выражения прямо там, где они используются. С их помощью можно легко определять функции обратного вызова, передавать их в функции высшего порядка или использовать для инициализации функциональных объектов.
Лямбда-выражения в C++11
Лямбда-выражение в C++11 можно объявить следующим образом:
auto myLambda = []() { /* тело лямбды */ };
[]
называется лямбда-интродуктором и указывает на захват переменных из окружающего контекста. Скобки после интродуктора могут содержать параметры, как у обычной функции, а фигурные скобки ограничивают тело лямбды.
Захват переменных позволяет лямбда-выражениям использовать данные из окружающего контекста. Доступны следующие способы захвата:
[=]
захватывает все переменные из области видимости по значению.[&]
захватывает все переменные по ссылке.[this]
захватывает указательthis
для доступа к членам класса.[var]
или[&var]
захватывает конкретную переменную по значению или по ссылке соответственно.
Простой пример:
std::vector<int> nums = {1, 2, 3, 4, 5};
std::for_each(nums.begin(), nums.end(), [](int n) { std::cout << n << " "; });
Код выводит все элементы вектора, используя лямбда-выражение в качестве функции обратного вызова.
Захват переменных:
int multiplier = 5;
auto multiply = [multiplier](int n) { return n * multiplier; };
std::cout << multiply(10); // Выведет 50
Лямбда захватывает переменную multiplier
по значению и использует ее для умножения входного параметра.
Лямбда в C++14
Одна из лучших фич в C++14 была связана с автоматическим выводом типов возвращаемого значения лямбда-выражений. Т.е наконец не обязательно явно указывать тип возвращаемого значения — компилятор может самостоятельно его определить. И кстати, C++14 ввел возможность инициализации захваченных переменных прямо в лямбда-интродукторе
Итак, в C++11 тип возвращаемого значения лямбда-выражения нужно было указывать явно, если оно не было очевидным. В C++14 этого уже не требуется благодаря автоматическому выводу типов:
Пример без C++14:
auto lambda = []() -> int { return 42; };
Пример с C++14:
auto lambda = []() { return 42; }; // тип значения int выводится автоматически
C++14 позволяет инициализировать переменные непосредственно в лямбда-интродукторе:
auto lambda = [x = 42]() { return x; };
x
инициализируется значением 42
и захватывается лямбда-выражением.
Пример с автоматическим выводом типов:
std::vector<int> nums = {1, 2, 3, 4, 5};
auto evenCount = std::count_if(nums.begin(), nums.end(), [](int n) { return n % 2 == 0; });
std::cout << "Количество четных чисел: " << evenCount << std::endl;
Лямбда-выражение используется для подсчета четных чисел в векторе, и тип возвращаемого значения выводится автоматически.
Пример с инициализацией захваченных переменных:
auto adder = [value = 10](int x) { return x + value; };
std::cout << adder(5); // 15
Лямбда-выражение захватывает и иницилизирует переменную value
, которая затем используется для добавления к переданному аргументу.
Нововведения в C++17
Здесь появилась крутая фича с захватом*this
в лямбда-выражениях. Можно захватывать текущий объект по значению, мастхев при работе с классами или объектами
Рассмотрим класс, в котором нужно использовать лямбда-выражение, захватывающее все локальные переменные по значению и текущий объект класса тоже по значению:
class MyClass {
public:
int value = 10;
void printValue() {
auto lambda = [=, *this]() {
std::cout << "Value: " << value << std::endl;
};
lambda();
}
};
int main() {
MyClass obj;
obj.printValue(); // Выведет: Value: 10
}
Лямбда захватывает текущий объект *this
по значению и так можно использовать члены класса внутри лямбды.
В C++17 лямбды теперь могут быть выполнены на этапе компиляции(constexpr)
:
constexpr auto square = [](int n) constexpr {
return n * n;
};
int main() {
constexpr int result = square(5);
static_assert(result == 25, "The result must be 25");
}
square
является constexpr
лямбда-выражением, а так его можно вычислить на этапе компиляции.
Также C++17 наконец стали довольно часто юзать stl. Вообще, можно было юзать и начиная с версии 14, но только в 17 версии это обрело свою массовость:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// auto в параметрах лямбда-выражения
auto isEven = [](auto n) { return n % 2 == 0; };
auto it = std::find_if(numbers.begin(), numbers.end(), isEven);
if (it != numbers.end()) {
std::cout << "Первое четное число: " << *it << std::endl;
}
}
Параметр auto
позволяет его использовать с любым типом, поддерживающим операцию %
.
C++20
C версии C++20 лямбду можно использовать вместе с шаблонами:
auto genericLambda = []<typename T>(T x) {
std::cout << x << std::endl;
};
genericLambda(10); // воркает с int
genericLambda("Test"); // с const char*
Концепты в C++20 позволили реализовать строгую типизацию и сделать более понятные требования к шаблонам:
#include <concepts>
#include <iostream>
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
auto add = []<Addable T>(T a, T b) {
return a + b;
};
int main() {
std::cout << add(1, 2) << std::endl; // 3
// std::cout << add("a", "b") << std::endl; // ошибка компиляции, так как "a" и "b" не удовлетворяют концепту Addable
}
Единственная константа в технологиях — это изменение. Лямбда-выражения делают код только более компактным и читаемым, а с каждой новой версией C++ их использование становится все более удобней.
Статья подготовлена в рамках запуска специализации C++. Хочу поделиться с вами ссылками на бесплатные вебинары специализации: