Пожалуйста, посмотрите мою небольшую статью в блоге, где я покажу вам несколько интересных примеров лямбд. Знаете ли вы, как написать рекурсивную лямбду? Хранить их в контейнере? Или вызывать во время компиляции?

Смотрите в статье.

1. Рекурсивная лямбда с помощью std::function 

Написать рекурсивную функцию относительно просто: внутри определения функции вы можете вызвать ту же функцию по ее имени. А как насчет лямбд?

int main() {
    auto factorial = [](int n) {
        return n > 1 ? n * factorial(n - 1) : 1;
    };
    return factorial(5);
}

Это, к сожалению, не компилируется...

Как можно все исправить?

Один из способов - использовать std::function:

#include <functional>

int main() {
    const std::function<int(int)> factorial = [&factorial](int n) {
        return n > 1 ? n * factorial(n - 1) : 1;
    };
    return factorial(5);
}

На этот раз нам нужно захватить factorial, а затем мы можем ссылаться на него внутри тела лямбды.

Начиная с C++14 мы также можем использовать общие лямбды и написать следующий код:

int main() {
    const auto factorial = [](int n) {
        const auto fact_impl = [](int n, const auto& impl) -> int {
            return n > 1 ? n * impl(n - 1, impl) : 1;
        };
        return fact_impl(n, fact_impl);
    };
    return factorial(5);
}

На этот раз он еще сложнее (но не требует интенсивного использования  std::function). Здесь внутренняя лямбда используется для основного вычисления, а затем она передается как общий аргумент.

Но мне интересно: вы когда-нибудь использовали рекурсивные лямбды? Или лучше полагаться на рекурсивные функции (которые кажутся гораздо более удобными в использовании и написании).

2. constexpr Lambdas

Но это еще не все с рекурсией... :)

Начиная с C++17 мы можем писать лямбды, у которых оператор вызова определен как constexpr. Можно использовать это свойство и расширить рекурсивный пример до:

int main() {
    constexpr auto factorial = [](int n) {
        constexpr auto fact_impl = [](int n, const auto& impl) -> int {
            return n > 1 ? n * impl(n - 1, impl) : 1;
        };
        return fact_impl(n, fact_impl);
    };
    static_assert(factorial(5) == 120);
}

А в C++20 вы даже можете применять consteval для маркировки лямбд, которые могут быть вычислены только во время компиляции.

3. Хранение лямбд в контейнере

Возможно, это не совсем правильно... но теоретически мы можем хранить лямбды в контейнере.

Хотя у типов замыканий конструкторы по умолчанию удалены (если только это не stateless lambda в C++20), можно сделать небольшой хак и хранить все лямбды как объекты  std::function. Например:

#include <functional>
#include <iostream>
#include <vector>

int main() {
    std::vector<std::function<std::string(const std::string&)>> vecFilters;
    
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " Amazing"; 
    });
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " Modern"; 
    });
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " C++"; 
    });
    vecFilters.emplace_back([](const std::string& x) { 
        return x + " World!"; 
    });
    
    const std::string str = "Hello";
    auto temp = str;
    
    for (auto &entryFunc : vecFilters)  
        temp = entryFunc(temp);
    
    std::cout << temp;
}

4. Общие лямбды и их вывод

C++14 привнес важное дополнение в лямбды: общие лямбда-аргументы. Приведем один пример, который показывает, в чем его польза:

#include <algorithm>
#include <iostream>
#include <map>
#include <string>

int main() {
    const std::map<std::string, int> numbers { 
        { "one", 1 }, {"two", 2 }, { "three", 3 }
    };
    
    std::for_each(std::begin(numbers), std::end(numbers), 
         [](const std::pair<std::string, int>& entry) {
             std::cout << entry.first << " = " << entry.second << '\n';
         }
    );
}

Вы знаете, в чем здесь ошибка? Правильно ли указан тип аргумента во внутренней лямбде for_each?

Я указал: const std::pair<std::string, int>& entry.

Но это неправильно, так как тип пары ключ/значение внутри карты таков:

std::pair<const std::string, int>.

Поэтому компилятор вынужден создавать ненужные временные копии и затем передавать их в мою лямбду.

Мы можем быстро исправить это, используя общую лямбду из C++14.

std::for_each(std::begin(numbers), std::end(numbers), 
    [](const auto& entry) {
        std::cout << entry.first << " = " << entry.second << '\n';
    }
);

Теперь типы совпадают, и дополнительные копии не создаются.

5. Возвращение лямбды

Если вы хотите вернуть лямбду из функции (например, для частичного применения функции, карринга), то это не так просто, поскольку вы не знаете точный тип объекта замыкания.

В C++11 одним из способов было использование std::function:

#include <functional>

std::function<int(int)> CreateLambda(int y) {
    return [&y](int x) { return x + y; };
}

int main() {
    auto lam = CreateLambda(10);
    return lam(32);
}

Но начиная с C++14, мы можем воспользоваться автоматическим выводом типов для их возвращения и просто написать:

auto CreateLambda(int y) {
    return [&y](int x) { return x + y; };
}

int main() {
    auto lam = CreateLambda(10);
    return lam(32);
}

Приведенный выше код намного проще и экономичнее, поскольку нам не нужно использовать std::function.

Резюме

В этой небольшой статье я показал вам пять интересных примеров лямбд. Они могут быть не совсем обычными, но показывают гибкость и иногда даже сложность типов замыкания.

Используете ли вы лямбды в подобных контекстах? А может быть, у вас есть еще более сложные примеры? Поделитесь своим опытом в комментариях под статьей.


Материал подготовлен в рамках курса «C++ Developer. Professional».

Всех желающих приглашаем на открытый урок «C++20: Корутины». На этом открытом уроке:
— разберем понятие сопрограмм (coroutines), их классификацию,
— детально рассмотрим реализацию, допущения и компромиссы, предлагаемые новым стандартом C++;
— разберём пользовательские типы, которые добавились для реализации сопрограмм (Promise, Awaitable.);
— разберём пример реализации асинхронного сетевого взаимодействия с использованием сопрограмм.

РЕГИСТРАЦИЯ

Комментарии (15)


  1. tangro
    09.09.2021 18:54
    +1

    Забавная, конечно, дичь. Практическое применение я смог придумать только примеру №3. Можно забабахать такую-себе ручную таблицу виртуальных функций вне контекста класса. Не знаю зачем, но можно.


    1. psycha0s
      09.09.2021 20:52
      +1

      Ну, хранение лямбд в контейнере может быть полезно при реализации механизма сигналов-слотов. При генерации сигнала, все функции в контейнере (слоты) вызываются по очереди.


  1. tony-space
    09.09.2021 19:23
    +4

    Автор оригинала забыл опустить наверное самую важную особенность лямбд, введённую с C++14 -- это generic lambda. В сочетании с fold expression (c++ 17) можно теперь не городить лютые шаблоны, а делать всё куда лакончинее и проще:

    #include <iostream>

    #include <string>

    int main()

    {

        auto sum = [](auto x, auto y, auto... args)

        {

            return ((x + y) + ... + args);

        };

        //prints: 8

        std::cout << sum(3, 5) << std::endl; 

        //prints: hello world one two 

        std::cout << sum(std::string{"hello "}, "world ", "one ", "two") << std::endl;

        return 0;

    }

    Фактически, лямбды стали синтаксическим сахаром над шаблонными функторами.


  1. KudryashovDA
    09.09.2021 21:50

    Мы на курсе делали интерпретатор упрощенного аналога Python. Там как раз функциональные объекты (выражения) сначала добавлялись в контейнер, а потом последовательно из него вызывались. Та была еще задачка.


  1. Playa
    09.09.2021 23:19
    +2

    Как обычно, переводчик изнасиловал статью:

    Общие лямбды и их вывод

    Возвращение лямбды


  1. old2ev
    09.09.2021 23:36
    +1

    Вот пример аналога рекурсивной лямбда-функции без std::function:

    struct {
      void operator()() {
        return (*this)();
      }
    } recusive_lambda;
    recursive_lambda();

    Да, это анонимная структура, но после компиляции останется лишь вызов перегрузки оператора, результат будет идентичен объявлению и вызову обычной лямбда-функции, но теперь у нас есть указатель this... но вот только теперь мы жертвуем лямбда-захватом, что в принципе сложно эмитировать при некоторых обстаятельствах. Кстати этот вариант лямбды можно засунуть в std::function , а следовательно и передовать в аргументы других функций, главное чтобы сигнатура перегрузки оператора совпадала с сигнатурой объявленной в std::function


    1. vanxant
      10.09.2021 01:46
      +1

      Захват можно делать руками через конструктор. Но с лайфтаймом придется повозиться.


  1. gwg605
    10.09.2021 02:26
    +1

    А можно поподробнее по поводу: Хранение лямбд в контейнере + Возможно, это не совсем правильно... + можно сделать небольшой хак и хранить все лямбды как объекты std::function
    ?
    Я себя данный подход использую во всю. Почему это не совсем правильно? почему хак? Я не прав так делая? Какие подводне камни здесь есть?


  1. nickolaym
    10.09.2021 04:34
    +2

    Рекурсивные лямбды - это, внезапно, именно то, как делаются рекурсивные функции в лямбда-исчислении. С помощью Y-комбинатора.

    Как по-быстрому и по-красивому прикрутить сюда карринг, у меня сейчас фантазии не хватило, поэтому фиг с ним, пусть будет бойлерплейт с вызовом g(g,....).

    auto Y = [](auto f) { return [f](auto x) { return f(f, x); }; };
    
    int main() {
      auto fac = Y([](auto g, unsigned k) -> unsigned { return k==0 ? 1 : k*g(g, k-1); });
    
      printf("%u", fac(5));
    }
    


  1. cr0nk
    10.09.2021 08:15
    +2

    Шел 2021 год. Я зык C++ медленно но верно деградировал в perl, где вместо того чтобы разбираться что хотел сказать тем или иным кодом очередной шизофреник было легче его переписать.


  1. rafuck
    10.09.2021 08:37
    +2

    auto CreateLambda(int y) {
        return [&y](int x) { return x + y; };
    }

    Тут мы точно хотим захватывать y по ссылке?


  1. Izaron
    11.09.2021 14:20

    Есть предложение в стандарт, которое вероятно попадет в ближайшие версии C++, в нем вводятся рекурсивные лямбды - self это неявный аргумент, являет собой объект closure.

    auto fib = [](this auto self, int n) {
        if (n < 2) return n;
        return self(n-1) + self(n-2);
    };


    1. rafuck
      11.09.2021 16:16

      А почему сразу this не сделать доступным внутри лямбды? Все равно ведь это класс с оператором вызова. Использовал this в теле лямбды — сказал компилятору: «не оптимизируй/не инлайнь».


      1. old2ev
        24.11.2021 12:47

        Исчезнет возможность подхватывать this класса где создаётся лямбда через лямбда-захват:

        class SomeClass {
        	int a = 255;
        public:
          auto getLambda();
        };
        
        auto SomeClass::getLambda() {
        	return [this](int a) {return a * this->a;}
        }


        1. rafuck
          24.11.2021 13:48

          Ох, да, правда ваша.