Новости

[Перевод] 5 любопытных примеров лямбд в C++: рекурсия, constexpr, контейнеры и многое другое

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

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

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.);
— разберём пример реализации асинхронного сетевого взаимодействия с использованием сопрограмм.
>> РЕГИСТРАЦИЯ

Добавить комментарий

Кнопка «Наверх»