Пожалуйста, посмотрите мою небольшую статью в блоге, где я покажу вам несколько интересных примеров лямбд. Знаете ли вы, как написать рекурсивную лямбду? Хранить их в контейнере? Или вызывать во время компиляции?
Смотрите в статье.
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)
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;
}
Фактически, лямбды стали синтаксическим сахаром над шаблонными функторами.
KudryashovDA
09.09.2021 21:50Мы на курсе делали интерпретатор упрощенного аналога Python. Там как раз функциональные объекты (выражения) сначала добавлялись в контейнер, а потом последовательно из него вызывались. Та была еще задачка.
Playa
09.09.2021 23:19+2Как обычно, переводчик изнасиловал статью:
Общие лямбды и их вывод
Возвращение лямбды
old2ev
09.09.2021 23:36+1Вот пример аналога рекурсивной лямбда-функции без
std::function
:struct { void operator()() { return (*this)(); } } recusive_lambda; recursive_lambda();
Да, это анонимная структура, но после компиляции останется лишь вызов перегрузки оператора, результат будет идентичен объявлению и вызову обычной лямбда-функции, но теперь у нас есть указатель
this
... но вот только теперь мы жертвуем лямбда-захватом, что в принципе сложно эмитировать при некоторых обстаятельствах. Кстати этот вариант лямбды можно засунуть вstd::function
, а следовательно и передовать в аргументы других функций, главное чтобы сигнатура перегрузки оператора совпадала с сигнатурой объявленной вstd::function
vanxant
10.09.2021 01:46+1Захват можно делать руками через конструктор. Но с лайфтаймом придется повозиться.
gwg605
10.09.2021 02:26+1А можно поподробнее по поводу: Хранение лямбд в контейнере + Возможно, это не совсем правильно... + можно сделать небольшой хак и хранить все лямбды как объекты
std::function
?
Я себя данный подход использую во всю. Почему это не совсем правильно? почему хак? Я не прав так делая? Какие подводне камни здесь есть?
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)); }
cr0nk
10.09.2021 08:15+2Шел 2021 год. Я зык C++ медленно но верно деградировал в perl, где вместо того чтобы разбираться что хотел сказать тем или иным кодом очередной шизофреник было легче его переписать.
rafuck
10.09.2021 08:37+2auto CreateLambda(int y) { return [&y](int x) { return x + y; }; }
Тут мы точно хотим захватывать y по ссылке?
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); };
rafuck
11.09.2021 16:16А почему сразу this не сделать доступным внутри лямбды? Все равно ведь это класс с оператором вызова. Использовал this в теле лямбды — сказал компилятору: «не оптимизируй/не инлайнь».
tangro
Забавная, конечно, дичь. Практическое применение я смог придумать только примеру №3. Можно забабахать такую-себе ручную таблицу виртуальных функций вне контекста класса. Не знаю зачем, но можно.
psycha0s
Ну, хранение лямбд в контейнере может быть полезно при реализации механизма сигналов-слотов. При генерации сигнала, все функции в контейнере (слоты) вызываются по очереди.