Лямбда-выражения — одна из самых популярных фич современного 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".
Комментарии (15)
kovserg
27.06.2022 23:07Интереснее всего передавать лямбду в виртуальную функцию.
victor79
28.06.2022 09:15-1auto fn1 = []() { std::cout << 123 << std::endl; }; using TT = decltype(fn1); struct A { virtual void a(TT fn) { fn(); } }; struct B : public A { void a(TT fn) override { fn(); fn(); } }; B a; a.a(fn1);
kovserg
28.06.2022 09:23+1a.a( []() { } ); // ups
victor79
28.06.2022 11:09-1Конечно упс, Вы же в нее пихаете значение другого типа. Какой вопрос, такой и ответ. Если такое хотите, то юзайте параметр типа function<void()>.
0x1b6e6
28.06.2022 17:23Замените одну строку:
using TT = std::function<void()>;
Будет работать. Только говорят что
std::function<T>
медленный. Ручаться не буду, но имейте ввиду.kovserg
28.06.2022 17:34-1Теперь следующий вопрос: как вы предлагаете экспортировать такую функцию из динамической библиотеки?
victor79
29.06.2022 05:06+1Там нужно типа такого:
virtual void a(const std::function<void()>& fn);
При этом использование std::function действительно чуть медленнее, но не существенно. Основные тормоза буду, если будет
void a(std::function<void()> fn)
, особенно при рекурсиях, т.к. объект std::function будет каждый раз конструироваться, и в довесок занимать лишние ~32 байта в стеке, против 8 байт, если передается по ссылке.kovserg
29.06.2022 09:19-1И как это потом использовать не из c++. Я к тому что лямбды сделаны чуть более чем
через жопуне правильно.
Что мешало сделать что-то подобное?#include <iostream> struct Callback { typedef void (*fn_t)(void *ctx); fn_t fn;void* ctx; void operator() () { if (fn) fn(ctx); } Callback(fn_t fn=0,void* ctx=0) : fn(fn), ctx(ctx) {} template<class Q>Callback(Q q) { struct L { static void fn(void *ctx) { Q* q=(Q*)ctx; (*q)(); } }; fn=L::fn; ctx=(void*)&q; } }; auto fn1 = []() { std::cout << 123 << std::endl; }; using TT = Callback; struct A { virtual void a(TT fn) { fn(); } }; struct B : public A { void a(TT fn) override { fn(); fn(); } }; int main(int argc, char const *argv[]) { B b; b.a(fn1); b.a([](){ std::cout<<sizeof(TT)<<std::endl; }); Callback f=fn1; f(); return 0; }
victor79
29.06.2022 20:46Все языки программирования имеют свои собственные форматы вызова функций, если только обратное не закладывалось заранее в этот язык. И эти форматы оптимальны пока используются в рамках этого языка. А между разными языками вызовы прорабатываются отдельно и создаются отдельные стандарты и форматы.
Ваш пример не оптимален для С++, поскольку не удобно поддерживать различные параметры вызова. std::function это шаблонная структура, как раз предком которой могла бы быть структура подобная Вашей.
И Ваш пример не будет универсальным для вызовов сторонними языками, т.к. как они каждый специфичен.
AndreyAf
Этот код выводит следующее:
конечно же нет.. будет ошибка:
no match for «operator<<» (operand types are «std::ostream» {aka «std::basic_ostream»} and «void»)
Начиная с C++14 мы можем заставить их принимать любой тип:
auto myLambda = [](auto&& x){ std::cout << x << '\n'; };
очень странно использовать здесь move симантику, если это не threads
AndreySu
Не понятно в чём проблема?
4eyes
Да, автор опечатался, кроме указания опечатки можно предлагать исправления:
Универсальная ссылка, может быть lvalue, может быть rvalue. Еще и проще читается, чем const auto& x. По-моему, сплошные преимущества.
Rahl
Здесь не move-симантика, а perfect forwarding. И как move-семантика коррелирует с threads?
PS: Смысл данной статьи не понимаю от слова совсем, какая-то протокольная распечатка эпизода Cᐩᐩ Weekly. В чем полезность?