[C++] Использование лямбда-выражений в необобщённом коде C++
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Появившиеся в C++11 лямбды стали одной из самых крутых фич нового стандарта языка, позволив сделать обобщённый код более простым и читабельным. Каждая новая версия стандарта C++ добавляет новые возможности лямбдам, делая обобщённый код ещё проще и читабельнее. Вы заметили, что слово «обобщённый» повторилось дважды? Это неспроста – лямбды действительно хорошо работают с кодом, построенным на шаблонах. Но при попытке использовать их в необобщённом, построенном на конкретных типах коде, мы сталкиваемся с рядом проблем. Статья о причинах и путях решения этих проблем.Вместо введенияДля начала определимся с терминологией: лямбдой мы называем lambda-expression – это выражение C++, определяющее объект замыкания (closure object). Вот цитата из стандарта C++:
[expr.prim.lambda.general]
A lambda-expression is a prvalue whose result object is called the closure object.
[Note 1: A closure object behaves like a function object. — end note]
Тип объекта замыкания – это уникальный безымянный класс.
[expr.prim.lambda.closure]
The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below.
«Безымянный» в данном случае означает, что тип замыкания нельзя явно указать в коде, но получить его можно, чем мы ниже будем активно пользоваться. «Уникальный» означает, что каждая лямбда порождает новый тип замыкания, т. е. две абсолютно одинаковые с синтаксической точки зрения лямбды (будем называть такие лямбды однородными) имеют разные типы:
auto l1 = [](int x) { return x; };
auto l2 = [](int x) { return x; };
static_assert(!std::is_same_v<decltype(l1), decltype(l2)>);
Аналогично этот принцип распространяется и на обобщённый код, зависящий от типов замыканий:
template <typename Func>
class LambdaDependent {
public:
explicit LambdaDependent(Func f) : f_{f} {}
private:
Func f_;
};
LambdaDependent ld1{l1};
LambdaDependent ld2{l2};
static_assert(!std::is_same_v<decltype(ld1), decltype(ld2)>);
Это свойство не позволяет, например, складывать объекты замыканий в контейнеры (например, в std::vector<>).Стандартные решенияСтандарт языка предоставляет готовое решение этой проблемы в виде std::function<>. Действительно, объект std::function<> может оборачивать однородные лямбды:
std::function f1{l1};
std::function f2{l2};
static_assert(std::is_same_v<decltype(f1), decltype(f2)>);
Такое решение действительно решает большинство проблем, однако подходит далеко не во всех случаях. Из объекта std::function<> нельзя получить сырой указатель на функцию, чтобы передать его, например, в какое-нибудь legacy API. Допустим, у нас есть функция:
int api_func(int(*fp)(int), int value) {
return fp(value);
}
Интересно, что если мы попробуем передать в эту функцию любую из объявленных выше лямбд (l1 или l2), то код замечательно скомпилируется и запустится:
std::cout << api_func(l1, 123) << '\n'; // 123
std::cout << api_func(l2, 234) << '\n'; // 234
Так получается потому, что лямбда с пустым замыканием (их ещё называют лямбды без состояния) по стандарту может быть неявно преобразована в указатель на функцию:
[expr.prim.lambda.closure]
The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type's function call operator. The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. The value returned by this conversion function is the address of a function F that, when invoked, has the same effect as invoking the closure type's function call operator on a default-constructed instance of the closure type. F is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function.
Для обобщённого кода можно применить явный static_cast<>:
LambdaDependent lf1{static_cast<int(*)(int)>(l1)};
LambdaDependent lf2{static_cast<int(*)(int)>(l2)};
static_assert(std::is_same_v<decltype(lf1), decltype(lf2)>);
Есть простой синтаксический трюк, позволяющий не писать громоздкий static_cast<> и не указывать явно сигнатуру функции:
LambdaDependent ls1{+l1};
LambdaDependent ls2{+l2};
static_assert(std::is_same_v<decltype(ls1), decltype(ls2)>);
Этот трюк работает из-за того, что унарный оператор + имеет встроенную перегрузку для любого типа.
Такая перегрузка, применённая к объекту замыкания, вызывает неявное преобразование к указателю на функцию, что аналогично явному static_cast<>.Лямбды с состояниемОписанный выше трюк отлично работает для лямбд с пустыми замыканиями, но как быть, если нужно передать лямбду с состоянием? Классический приём из старого доброго C – передавать указатель на функцию и указатель на контекст типа void*. Функция получает этот указатель, преобразовывает его к указателю на нужный тип и получает доступ к контексту.
int api_func_ctx(int(*fp)(void*, int), void* ctx, int value) {
return fp(ctx, value);
}
Попробуем передать лямбду с состоянием в такую функцию:
int counter = 1;
auto const_lambda = [counter](int value) {
return value + counter;
};
std::cout << api_func_ctx([](void* ctx, int value) {
auto* lambda_ptr = static_cast<decltype(const_lambda)*>(ctx);
return (*lambda_ptr)(value);
}, &const_lambda, 123) << '\n'; // 124
Здесь мы задаём новую лямбду, уже без состояния, которая неявно преобразуется в указатель на функцию. Эта лямбда получает контекст типа void*, преобразует его к указателю на тип замыкания, разыменовывает и вызывает как обычный функциональный объект. Кстати, это работает и с mutable лямбдами:
auto mutable_lambda = [&counter](int value) mutable {
++counter;
return value * counter;
};
std::cout << api_func_ctx([](void* ctx, int value) {
auto* lambda_ptr = static_cast<decltype(mutable_lambda)*>(ctx);
return (*lambda_ptr)(value);
}, &mutable_lambda, 123) << ':' << counter << '\n'; // 246:2
Кажется, всё уже отлично работает. Но писать лямбду руками при каждом вызове api_func_ctx утомительно, хочется всё это обобщить и завернуть в красивую обёртку.Наводим красотуТехнически для того, чтобы сохранить лямбду с состоянием, а потом восстановить её, достаточно 2х объектов:
- контекст типа void* (это классический пример type erasure);
- указатель на функцию, принимающую контекст и все параметры лямбды и возвращающую такой же тип.
Назовём тип для хранения «разобранного» объекта замыкания closure_erasure:
template <typename Ret, typename ...Args>
struct closure_erasure {
Ret(*func)(void*, Args...);
void* ctx;
};
Тут возникает проблема: как из типа замыкания выудить тип возвращаемого значения и параметров? На помощь нам приходит CTAD – этот тип может выводиться из типа параметра конструктора. Но что такого можно передать в конструктор от лямбды, из чего можно вывести все необходимые типы? Компилятор определяет для типа замыкания operator(), позволяющий вызывать объект замыкания как функциональный объект. Сигнатура этого оператора как раз и содержит всю необходимую информацию:
template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...), void* ctx) :
func{
[](void* c, Args ...args) {
auto* lambda_ptr = static_cast<Lambda*>(c);
return (*lambda_ptr)(std::forward<Args>(args)...);
}
},
ctx{ctx} {}
template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...) const, void* ctx) :
func{
[](void* c, Args ...args) {
auto* lambda_ptr = static_cast<Lambda*>(c);
return (*lambda_ptr)(std::forward<Args>(args)...);
}
},
ctx{ctx} {}
Два конструктора отличаются только const в типе указателя на метод класса – это нужно для того, чтобы можно было оборачивать и обычные (константные), и mutable лямбды.Остался последний шаг: обернуть создание обёртки в удобную функцию, чтобы не нужно было вручную извлекать указатель на operator() из типа замыкания:
auto make_closure_erasure = [](auto& lmb) {
return closure_erasure{
&std::remove_reference_t<decltype(lmb)>::operator(), &lmb};
};
Обратите внимание, что мы принимаем объект замыкания по неконстантной ссылке. Это важный момент, который намекает на важное ограничение применяемого механизма: мы несём ответственность за время жизни объекта замыкания!Если мы ещё вспомним, что функция и лямбда может быть ещё noexcept, то финальная версия обёртки будет выглядеть так:
template <typename Ret, bool NoExcept, typename ...Args>
struct closure_erasure {
Ret(*func)(void*, Args...) noexcept(NoExcept);
void* ctx;
template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...) noexcept(NoExcept), void* ctx) :
func{
[](void* c, Args ...args) noexcept(NoExcept) {
auto* lambda_ptr = static_cast<Lambda*>(c);
return (*lambda_ptr)(std::forward<Args>(args)...);
}
},
ctx{ctx} {}
template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...) const noexcept(NoExcept), void* ctx) :
func{
[](void* c, Args ...args) noexcept(NoExcept) {
auto* lambda_ptr = static_cast<Lambda*>(c);
return (*lambda_ptr)(std::forward<Args>(args)...);
}
},
ctx{ctx} {}
};
auto make_closure_erasure = [](auto& lmb) {
return closure_erasure{
&std::remove_reference_t<decltype(lmb)>::operator(), &lmb};
};
auto li = make_closure_erasure(const_lambda);
std::cout << api_func_ctx(li.func, li.ctx, 123) << '\n'; // 124
li = make_closure_erasure(mutable_lambda);
std::cout << counter << ':' <<
api_func_ctx(li.func, li.ctx, 123) << '\n'; // 2:369
std::cout << counter << ':' <<
api_func_ctx(li.func, li.ctx, 123) << '\n'; // 3:492
Интересные ссылки
- Compiler Explorer с кодом из статьи
- Книга про лямбды "C++ Lambda Story"
- Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
- C++ Weekly - Ep 246 - (+[](){})() What Does It Mean?
- Текущий черновик стандарта C++
Большое спасибо Валерию Артюхину за корректуру.
===========
Источник:
habr.com
===========
Похожие новости:
- [C++, Параллельное программирование] Часть 2. MPI — Учимся следить за процессами
- [Программирование, C++] Новый поток в C++20: std::jthread (перевод)
- [C++, Алгоритмы, Параллельное программирование] Часть 1. MPI — Введение и первая программа
- [Программирование микроконтроллеров, Электроника для начинающих] На распутье — Ардуино, Cи или Ассемблер?
- [Программирование, C++, Программирование микроконтроллеров, Производство и разработка электроники] Создание аналога посмертного сore dump для микроконтроллера
- [Open source, C++, Отладка, Разработка под Windows] Полезные скрипты для WinDBG: команда !exccandidates
- [Программирование, C++, Распределённые системы, Микросервисы] С чего начать писать микросервис на C++
- [Программирование, Совершенный код, C++, Лайфхаки для гиков] Прочти меня: код, который не выбесит соседа
- [C++] Доступ к элементам std::tuple во время исполнения программы
- [Программирование, C++, Промышленное программирование, Программирование микроконтроллеров] Маленькие хитрости для STM32
Теги для поиска: #_c++, #_c++, #_lambdas, #_closures, #_c++
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:04
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Появившиеся в C++11 лямбды стали одной из самых крутых фич нового стандарта языка, позволив сделать обобщённый код более простым и читабельным. Каждая новая версия стандарта C++ добавляет новые возможности лямбдам, делая обобщённый код ещё проще и читабельнее. Вы заметили, что слово «обобщённый» повторилось дважды? Это неспроста – лямбды действительно хорошо работают с кодом, построенным на шаблонах. Но при попытке использовать их в необобщённом, построенном на конкретных типах коде, мы сталкиваемся с рядом проблем. Статья о причинах и путях решения этих проблем.Вместо введенияДля начала определимся с терминологией: лямбдой мы называем lambda-expression – это выражение C++, определяющее объект замыкания (closure object). Вот цитата из стандарта C++: [expr.prim.lambda.general]
A lambda-expression is a prvalue whose result object is called the closure object. [Note 1: A closure object behaves like a function object. — end note] [expr.prim.lambda.closure]
The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below. auto l1 = [](int x) { return x; };
auto l2 = [](int x) { return x; }; static_assert(!std::is_same_v<decltype(l1), decltype(l2)>); template <typename Func>
class LambdaDependent { public: explicit LambdaDependent(Func f) : f_{f} {} private: Func f_; }; LambdaDependent ld1{l1}; LambdaDependent ld2{l2}; static_assert(!std::is_same_v<decltype(ld1), decltype(ld2)>); std::function f1{l1};
std::function f2{l2}; static_assert(std::is_same_v<decltype(f1), decltype(f2)>); int api_func(int(*fp)(int), int value) {
return fp(value); } std::cout << api_func(l1, 123) << '\n'; // 123
std::cout << api_func(l2, 234) << '\n'; // 234 [expr.prim.lambda.closure]
The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type's function call operator. The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. The value returned by this conversion function is the address of a function F that, when invoked, has the same effect as invoking the closure type's function call operator on a default-constructed instance of the closure type. F is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function. LambdaDependent lf1{static_cast<int(*)(int)>(l1)};
LambdaDependent lf2{static_cast<int(*)(int)>(l2)}; static_assert(std::is_same_v<decltype(lf1), decltype(lf2)>); LambdaDependent ls1{+l1};
LambdaDependent ls2{+l2}; static_assert(std::is_same_v<decltype(ls1), decltype(ls2)>); int api_func_ctx(int(*fp)(void*, int), void* ctx, int value) {
return fp(ctx, value); } int counter = 1;
auto const_lambda = [counter](int value) { return value + counter; }; std::cout << api_func_ctx([](void* ctx, int value) { auto* lambda_ptr = static_cast<decltype(const_lambda)*>(ctx); return (*lambda_ptr)(value); }, &const_lambda, 123) << '\n'; // 124 auto mutable_lambda = [&counter](int value) mutable {
++counter; return value * counter; }; std::cout << api_func_ctx([](void* ctx, int value) { auto* lambda_ptr = static_cast<decltype(mutable_lambda)*>(ctx); return (*lambda_ptr)(value); }, &mutable_lambda, 123) << ':' << counter << '\n'; // 246:2
template <typename Ret, typename ...Args>
struct closure_erasure { Ret(*func)(void*, Args...); void* ctx; }; template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...), void* ctx) : func{ [](void* c, Args ...args) { auto* lambda_ptr = static_cast<Lambda*>(c); return (*lambda_ptr)(std::forward<Args>(args)...); } }, ctx{ctx} {} template<typename Lambda> explicit closure_erasure(Ret(Lambda::*)(Args...) const, void* ctx) : func{ [](void* c, Args ...args) { auto* lambda_ptr = static_cast<Lambda*>(c); return (*lambda_ptr)(std::forward<Args>(args)...); } }, ctx{ctx} {} auto make_closure_erasure = [](auto& lmb) {
return closure_erasure{ &std::remove_reference_t<decltype(lmb)>::operator(), &lmb}; }; template <typename Ret, bool NoExcept, typename ...Args>
struct closure_erasure { Ret(*func)(void*, Args...) noexcept(NoExcept); void* ctx; template<typename Lambda> explicit closure_erasure(Ret(Lambda::*)(Args...) noexcept(NoExcept), void* ctx) : func{ [](void* c, Args ...args) noexcept(NoExcept) { auto* lambda_ptr = static_cast<Lambda*>(c); return (*lambda_ptr)(std::forward<Args>(args)...); } }, ctx{ctx} {} template<typename Lambda> explicit closure_erasure(Ret(Lambda::*)(Args...) const noexcept(NoExcept), void* ctx) : func{ [](void* c, Args ...args) noexcept(NoExcept) { auto* lambda_ptr = static_cast<Lambda*>(c); return (*lambda_ptr)(std::forward<Args>(args)...); } }, ctx{ctx} {} }; auto make_closure_erasure = [](auto& lmb) { return closure_erasure{ &std::remove_reference_t<decltype(lmb)>::operator(), &lmb}; }; auto li = make_closure_erasure(const_lambda); std::cout << api_func_ctx(li.func, li.ctx, 123) << '\n'; // 124 li = make_closure_erasure(mutable_lambda); std::cout << counter << ':' << api_func_ctx(li.func, li.ctx, 123) << '\n'; // 2:369 std::cout << counter << ':' << api_func_ctx(li.func, li.ctx, 123) << '\n'; // 3:492
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:04
Часовой пояс: UTC + 5