[Перевод] Эволюция лямбд в C++14, C++17 и C ++20

24f43e4ee3dfea3b3107d2bda9f4a40f.png

Лямбда-выражения — одна из самых популярных фич современного 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 = [](T&& value){ std::cout << value << '\n'; };

Это упрощает доступ к типу шаблонного параметра по сравнению с шаблонными лямбда-выражениями C++14, в которых использовались такие выражения, как auto&&.

Другим улучшением является возможность захвата вариативного (variadic) пакета параметров:

template
void f(Ts&&... args)
{
    auto myLambda = [...args = std::forward(args)](){};
}

Погружение в лямбды

Мы рассмотрели то, что я считаю основными улучшениями лямбда-выражений от C++14 до C++20. Но это еще не все. Эти важные фичи идут в сопровождении ряда небольших улучшений, которые упрощают написание лямбда-кода.

Более глубокое погружение в лямбда-выражения — это отличная возможность лучше понять язык C++, и я думаю, что это стоящая инвестиция времени. Чтобы пойти дальше, лучший известный мне ресурс — это книга Бартоломея Филипика C++ Lambda Story, которую я уже рекомендовал вам.

Перевод статьи подготовлен в преддверии старта специализации «C++ Developer».

© Habrahabr.ru