Лямбда-выражения — одна из самых популярных фич современного C++. С тех пор, как они были представлены в C++11, лямбды проникли практически в каждую кодовую базу на C++.
И с момента их появления в C++11 их не переставали развивать, добавляя серьезные фичи для работы с ними. Некоторые из этих фич помогают писать более выразительный код, и, поскольку использование лямбда-выражений стало таким распространенным, каждому из нас определенно стоит потратить немного времени на изучение того, что мы можем с ними делать.
Цель этой статьи — рассказать об основных эволюционных этапах в истории лямбда-выражений, опустив некоторые мелкие детали. Всесторонний обзор лямбда-выражений уже больше тянет на отдельную книгу, нежели небольшую статью. Если вы хотите узнать больше, я рекомендую вам почитать книгу Бартоломея Филипика (Bartłomiej Filipek) C++ Lambda Story, которая раскрывает эту тему целиком и полностью.
Общую эволюцию лямбда-выражений можно охарактеризовать как наделение их возможностями объектов-функций, определяемых вручную.
Эта статья требует от вас наличие базовых знаний о лямбда-выражениях C++11. Ну что ж, начнем с C++14.
Лямбда-выражения в C++14
В C++14 лямбда-выражения получили четыре серьезных усовершенствования:
параметры по умолчанию;
шаблонные параметры;
обобщенный захват;
возврат лямбды из функции.
Параметры по умолчанию
Начиная с C++14 лямбда-выражения могут принимать параметры по умолчанию, как и любая другая функция:
auto myLambda = [](int x, int y = 0){ std::cout << x << '-' << y << '\n'; };
std::cout << myLambda(1, 2) << '\n';
std::cout << myLambda(1) << '\n';
Этот код выводит следующее:
1-2
1-0
Шаблонные параметры
В C++11 мы должны определить тип параметров лямбда-выражений:
auto myLambda = [](int x){ std::cout << x << '\n'; };
Начиная с C++14 мы можем заставить их принимать любой тип:
auto myLambda = [](auto&& x){ std::cout << x << '\n'; };
Даже если вам не нужно обрабатывать несколько типов, эта фича может быть очень полезной, чтобы избежать повторений и сделать код более компактным и читабельным. Например, такая лямбда:
auto myLambda = [](namespace1::namespace2::namespace3::ACertainTypeOfWidget const& widget) { std::cout << widget.value() << '\n'; };
становится такой:
auto myLambda = [](auto&& widget) { std::cout << widget.value() << '\n'; };
Обобщенный захват
В C++11 лямбда-выражения могут захватывать только существующие в их области видимости объекты:
int z = 42;
auto myLambda = [z](int x){ std::cout << x << '-' << z + 2 << '\n'; };
Но с новым обобщенным лямбда-захватом мы можем инициализировать захватываемые значения практически чем угодно. Вот простой пример:
int z = 42;
auto myLambda = [y = z + 2](int x){ std::cout << x << '-' << y << '\n'; };
myLambda(1);
Этот код выводит следующее:
1-44
Возврат лямбда-выражения из функции
Лямбда-выражения приобрели кое-что для себя и благодаря другой фиче C++14: возможности возвращать auto из функции без указания возвращаемого типа. Поскольку тип лямбды генерируется компилятором, в C++11 мы не могли вернуть лямбду из функции:
/* какой тип нам следует здесь указать ?? */ f()
{
return [](int x){ return x * 2; };
}
В C++14 мы можем вернуть лямбду, используя auto в качестве типа возвращаемого значения. Это полезно в случаях больших лямбд, находящихся прямо посреди других фрагментов кода:
void f()
{
// ...
int z = 42;
auto myLambda = [z](int x)
{
// ...
// ...
// ...
};
// ...
}
Мы можем обернуть лямбду в другую функцию, тем самым введя новый уровень абстракции:
auto getMyLambda(int z)
{
return [z](int x)
{
// ...
// ...
// ...
};
}
void f()
{
// ...
int z = 42;
auto myLambda = getMyLambda(z);
// ...
}
Чтобы узнать больше об этом методе, советую вам почитать о внешних лямбда-выражениях.
Лямбда-выражения в C++17
C++17 привнес очень существенное улучшение в лямбда-выражения: их можно объявлять constexpr
:
constexpr auto times2 = [] (int n) { return n * 2; };
Затем такие лямбды можно использовать в контекстах, оцениваемых во время компиляции:
static_assert(times2(3) == 6);
Это особенно полезно при работе с шаблонами.
Однако следует отметить, что constexpr лямбды становятся гораздо более полезными в C++20. Действительно, только в C++20 std::vector и большинство алгоритмов STL также становятся constexpr, и их можно использовать с constexpr лямбдами для создания сложных манипуляций с коллекциями, оцениваемыми во время компиляции.
Однако есть одно исключение - контейнер std::array. Неизменяющие операции доступа std::array становятся constexpr в C++14, а изменяющие - в C++17.
Захват копии *this
Еще одна фича, которую лямбда-выражения получили в C++17, — это простой синтаксис для захвата копии *this
. Рассмотрим следующий пример:
struct MyType{
int m_value;
auto getLambda()
{
return [this](){ return m_value; };
}
};
Эта лямбда захватывает копию this
(указателя). Это может вызвать ошибки памяти, если лямбда переживет объект, например, как в следующем примере:
auto lambda = MyType{42}.getLambda();
lambda();
Поскольку MyType уничтожается в конце первого выражения, вызов лямбды во втором операторе разыменовывает this для доступа к его m_value
, а он указывает на уже уничтоженный объект. Это приводит к неопределенному поведению (обычно к крашу приложения).
Один из возможных способов решить эту проблему — захватить копию всего объекта внутри лямбды. C++17 предоставляет для этого следующий синтаксис (обратите внимание на *
перед this
):
struct MyType
{
int m_value;
auto getLambda()
{
return [*this](){ return m_value; };
}
};
Обратите внимание, что уже в C++14 можно было добиться такого же результата с помощью обобщенного захвата:
struct MyType
{
int m_value;
auto getLambda()
{
return [self = *this](){ return self.m_value; };
}
};
C++17 только улучшает этот синтаксис.
Лямбда-выражения в C++20
Лямбды продолжили свою эволюцию и в C++20, но на этот раз получили менее фундаментальные фичи, чем в C++14 или C++17.
Одним из усовершенствований лямбда-выражений в C++20, которое еще больше приближает их к объектам функций, определяемым вручную, является классический синтаксис для определения шаблонов:
auto myLambda = []<typename T>(T&& value){ std::cout << value << '\n'; };
Это упрощает доступ к типу шаблонного параметра по сравнению с шаблонными лямбда-выражениями C++14, в которых использовались такие выражения, как auto&&
.
Другим улучшением является возможность захвата вариативного (variadic) пакета параметров:
template<typename... Ts>
void f(Ts&&... args)
{
auto myLambda = [...args = std::forward<Ts>(args)](){};
}
Погружение в лямбды
Мы рассмотрели то, что я считаю основными улучшениями лямбда-выражений от C++14 до C++20. Но это еще не все. Эти важные фичи идут в сопровождении ряда небольших улучшений, которые упрощают написание лямбда-кода.
Более глубокое погружение в лямбда-выражения — это отличная возможность лучше понять язык C++, и я думаю, что это стоящая инвестиция времени. Чтобы пойти дальше, лучший известный мне ресурс — это книга Бартоломея Филипика C++ Lambda Story, которую я уже рекомендовал вам.
Перевод статьи подготовлен в преддверии старта специализации "C++ Developer".