Комментарии 41
И во-вторых, лямбда функция, захватывающая аргументы снаружи, может быть передана только в шаблонную функцию, у которой тип принимаемой функции является шаблонным параметром
Вы можете использовать обёртку в виде std::function<void(int)>
.
спасибо. Я постараюсь дополнить статью с учётом этого.
и почти гарантированную аллокацию памяти
зависит от того, насколько много добра вы ходите захватить
во всех известных мне компиляторах std::function имеет внутри себя буфер для того, чтобы в случае небольшого контекста не бежать сразу в кучу
function ref никогда не требует аллокации по очевидным причинам. А function почти никогда не будет аллоцировать лямбду, только если она какая то громадная или бросает исключение в муве или алигмент у неё какой то кривой, этого почти никогда не бывает.
Получше разберитесь в вопросе
в function ref нет никакой SOO и быть не может
Вообще-то это замыкания, а не лямбда-функции.
Замыкание — это свойство анонимной функции захватывать переменные из контекста.
Не обязательно анонимной.
Не свойство анонимной функции, а отдельный механизм, которого может и не быть, - как, к примеру, в С или даже в Лиспе. Сама по себе идея анонимной функции никаких захватов не содержит.
Идея анонимной функции ничем не отличается от анонимного экземпляра, скажем, структуры.
Другое дело, что реализация в разных языках, особенно без автоматического управления памятью, разной степени костыльная. И различие между именованными и неименованными функциями -- совершенный произвол дизайнеров конкретных языков, не обусловленный никакими фундаментальными причинами.
Что скажете про D?
import std.stdio;
extern void for_each( const int[], void delegate(int) );
void example( const int[] vec, int threshold ) {
for_each( vec, (n) {
if( n > threshold ) n.writeln;
} );
}
void onlineapp.example(const(int[]), int):
push RBP
mov RBP,RSP
sub RSP,030h
mov -030h[RBP],RBX
mov -020h[RBP],EDI
mov -010h[RBP],RSI
mov -8[RBP],RDX
mov EDI,0Ch
call _d_allocmemory@PLT32
mov RBX,RAX
mov qword ptr [RBX],0
mov EAX,-020h[RBP]
mov 8[RBX],EAX
mov RCX,-8[RBP]
mov RAX,-010h[RBP]
mov RDX,RAX
mov -028h[RBP],RDX
mov RDI,RBX
mov RDX,@safe void onlineapp.example(const(int[]), int).__lambda3!(int).__lambda3(int)@GOTPCREL[RIP]
mov RSI,RDX
mov RDX,-028h[RBP]
call void onlineapp.for_each(const(int[]), void delegate(int))@PLT32
mov RBX,-030h[RBP]
mov RSP,RBP
pop RBP
ret
add [RAX],AL
@safe void onlineapp.example(const(int[]), int).__lambda3!(int).__lambda3(int):
push RBP
mov RBP,RSP
sub RSP,010h
mov -8[RBP],ESI
mov ESI,-8[RBP]
cmp 8[RDI],ESI
jge L1E
mov -8[RBP],ESI
mov EDI,-8[RBP]
call @safe void std.stdio.writeln!(int).writeln(int)@PLT32
L1E: mov RSP,RBP
pop RBP
ret
add [RAX],AL
Скажу, что функция example принимает threshold на регистре %rdx; выделяет память на куче размером с то, что захватывает лямбда функция, копирует туда этот threshold, после чего передаёт в for_each на регистрах ваш массив, его размер, указатель на эту структуру с захваченным и указатель на код лямбда функции.
Лямбда функция первым параметром получает указатель на структуру с захваченным, вторым - собственный аргумент.
По сути, это подход с виртуальными классами, только указатель на код функции кладётся не в структуру с захваченными переменными, а отдельно.
Убрал выделение памяти:
import std.stdio;
extern void for_each( scope const int[], scope void delegate(int) ) @nogc;
void example( const int[] vec, int threshold ) @nogc {
for_each( vec, (n) {
if( n > threshold ) n.writeln;
} );
}
@nogc void onlineapp.example(const(int[]), int):
push RBP
mov RBP,RSP
sub RSP,030h
mov -020h[RBP],EDI
mov -010h[RBP],RSI
mov -8[RBP],RDX
mov RCX,-8[RBP]
mov RAX,-010h[RBP]
mov RDX,RAX
mov -028h[RBP],RDX
mov RDI,RBP
mov RDX,@safe void onlineapp.example(const(int[]), int).__lambda3!(int).__lambda3(int)@GOTPCREL[RIP]
mov RSI,RDX
mov RDX,-028h[RBP]
call @nogc void onlineapp.for_each(scope const(int[]), scope void delegate(int))@PLT32
mov RSP,RBP
pop RBP
ret
add [RAX],AL
add [RAX],AL
@safe void onlineapp.example(const(int[]), int).__lambda3!(int).__lambda3(int):
push RBP
mov RBP,RSP
sub RSP,010h
mov -8[RBP],ESI
mov ESI,-8[RBP]
cmp -020h[RDI],ESI
jge L1E
mov -8[RBP],ESI
mov EDI,-8[RBP]
call @safe void std.stdio.writeln!(int).writeln(int)@PLT32
L1E: mov RSP,RBP
pop RBP
ret
add [RAX],AL
Ну это тоже самое, только память выделяется на стеке
Но не мешает компилятору инлайнить, если это возможно:
import std.stdio;
void for_each( scope const int[] vec, scope const void delegate(int) del ) {
foreach( item; vec ) del( item );
};
void example( const int[] vec, int threshold ) {
for_each( vec, (n) {
if( n > threshold ) n.writeln;
} );
}
void onlineapp.example(const(int[]), int):
push RBP
mov RBP,RSP
sub RSP,030h
mov -028h[RBP],RBX
mov -020h[RBP],R12
mov -018h[RBP],R13
mov -010h[RBP],R14
mov -8[RBP],EDI
mov R14,RSI
mov R13,RDX
xor EBX,EBX
test R14,R14
je L43
L28: mov R12D,0[RBX*4][R13]
cmp R12D,-8[RBP]
jle L3B
mov EDI,R12D
call @safe void std.stdio.writeln!(int).writeln(int)@PLT32
L3B: inc RBX
cmp RBX,R14
jb L28
L43: mov RBX,-028h[RBP]
mov R12,-020h[RBP]
mov R13,-018h[RBP]
mov R14,-010h[RBP]
mov RSP,RBP
pop RBP
ret
А в чём преимущества по сравнению с обычной функцией? Или оно нужно только когда лень придумывать нормальный осмысленный говорящий сам за себя идентификатор?
В С++, например, это единственный способ написать функцию учитывающую локальный контекст другой функции. Можно, конечно, передать параметрами, но параметров таких может оказаться неоправданно много.
Кроме того, такие "одноразовые" функции, которые имеют смысл только в данном конкретном месте кода, не имеет смысла выносить за пределы этого контекста.
Это в языках, где есть вложенные функции (Паскаль, например) лямбды не особо нужны. Хотя и там возможность привязать функцию к конкретеому месту в коде (вложеные функции в том же Паскале объявляются только в самом начале другой функции) может оказаться полезной.
Можно, конечно, передать параметрами, но параметров таких может оказаться неоправданно много.
Замыкаемые переменные тоже надо указывать, поэтому выигрыш тут может быть только в том, что не нужно указывать типы данных.
Но это довольно сомнительное преимущество. Потому что экономя на осмысленном названии функции из нескольких символов получаем необходимость зубрить N страниц стандарта со всякими std::function, шаблонами, подводными камнями и нюансами применения.
Замыкаемые переменные тоже надо указывать
Не нужно, если способ захвата у них одинаковый.
Или оно нужно только когда лень придумывать нормальный осмысленный говорящий сам за себя идентификатор?
Это у вас от недостатка фантазии (а скорее всего - опыта). Как пример - вам нужно зарегистрировать где-то некие обработчики, которые должны захватывать данные разного типа. Например:
std::vector<std::function<void(size_t)>> v;
void foo()
{
struct T1 { std::string foo() const { return "T1"; } } t1;
struct T2 { std::string foo() const { return "T2"; } } t2;
v.emplace_back([t1](size_t i){ std::cout << t1.foo() << " " << i << std::endl; });
v.emplace_back([t2](size_t i){ std::cout << t2.foo() << " " << i << std::endl; });
}
void bar()
{
for (size_t i = 0; i < v.size(); ++i) v[i](i);
}
void some_other_function_1()
{
foo();
}
void some_other_function_2()
{
bar();
}
А теперь повторите это, пожалуйста, на "обычных функциях" и почувствуйте разницу. Кстати, как видите, захват в кооперации с std::function
прекрасно работает даже с локальными типами (T1
и T2
) - они недоступны за пределами foo()
, однако же bar()
это ничуть не смущает. Кстати, попытайтесь повторить этот эффект на "обычных функциях".
Замыкаемые переменные тоже надо указывать, поэтому выигрыш тут может быть только в том, что не нужно указывать типы данных.
Если вы передаете функцию в качестве параметра другой функции, то потребные для ее вызова дополнительные параметры придется передавать сначала в эту самую другую функцию. А к логике этой другой функции эти параметры могут никаким боком не относиться.
Конечно, можно было бы вместо лямбд позволить объявлять функции в любом месте программы (с соответствующей областью видимости и доступом к контексту), но в С++ пока есть только лямбды. Конечно завсегда можно объявить для этого целый класс, но это как-то избыточно и загромождает код лишними конструкциями.
А в отсутствии имени у одноразовой функции не вижу никакого криминала. Вы же не будете давать имя каджой операции при вычислении выражения, срдержащего более двух операций.
Конечно, можно было бы вместо лямбд позволить объявлять функции в любом месте программы (с соответствующей областью видимости и доступом к контексту)
Это не было бы заменой замыканиям, ибо суть замыканий не просто в доступе к контексту, но в его запоминании. Контекст вложенной функции в паскале жив только пока выполняется внешняя функция (живы ее данные на стеке). Замыкание же носит контекст с собой и он может быть использован даже если замыкание было куда-то записано, а затем вызвано совсем из другого места.
Это не было бы заменой замыканиям, ибо суть замыканий не просто в доступе к контексту, но в его запоминании.
Это уже тонкости реализации. Вполне можно при описании функции дать список запоминаемых переменных окружения или всегда сохранять все, что используется внутри функции, и в итоге получить практически ту же лямбду, но с персональным именем (особенно если допустить описание функции прямо в месте использования).
А можно поинтересоваться, какое вы применение этому имени видите? Так-то и у лямбды может быть "имя":
auto foo = [a, b, &c](d){ ... };
foo(...);
std::for_each(something.begin(), something.end(), foo);
Здесь у foo
будет некий уникальный тип, автоматически сгенерированный компилятором. А вызывать гипотетическую функцию bar()
, вложенную в foo()
, извне, наподобие чего-нибудь такого:
void foo()
{
[...]
void bar()
{
[...]
}
}
int main()
{
foo::bar();
}
все равно бессмысленно по очевидным причинам.
А можно поинтересоваться, какое вы применение этому имени видите?
Так некоторые люди тут этого хотят. Они считают, что лямбды для ленивых.
Для меня же это всего лишь возможность немного увеличить понятность кода. Лямбда как-то синтаксически не очень хорошо выделяется как функция. Глаз "соскальзывает". А полноценное определение вложенной функции уже как-то лучше воспринимается. Но это мелочи. С точки зрения использовании реальная разница между такой функцией и лямбдой будет зависеть от деталей конкретной реализации.
Так-то и у лямбды может быть "имя"
Строго говоря, это будет не имя лямбды, а имя переменной с указателем на лямбду. Понятно, что хоть совой об пень, что пнем об сову, но если докапываться до тонкостей, то докапываться до конца.
foo::bar();
Но зачем? Вложенная функция (точнее, ее имя) только для местного употребления. Наружу она только как параметр при вызове другой функции или возвращаемое значение должна вылезать.
А так-то можно и локальные переменные функции захотеть снаружи использовать по их локальным же именам.
Мне кажется объяснение с ассемблером получилось сложнее, чем могло бы быть с https://cppinsights.io
В C++ лямбда функция - просто синтаксический сахар для класса с перегруженным оператором вызова `ret operator()(args...)`.
Если у лямбды пустой список захвата, то в сгенерированный класс добавляется оператор приведения к указателю на функцию: https://cppinsights.io/s/dab279d2
Если список захвата не пустой, то в сгенерированный класс добавляются соответствующие поля (ссылки для & или значения для =) и приведение к указателю на функцию невозможно, так как для вызова требуется экземпляр лямбды: https://cppinsights.io/s/5e323673
Более того — ровно такое поведение стандартом C++ и описано: https://en.cppreference.com/w/cpp/language/lambda и https://eel.is/c++draft/expr.prim.lambda.closure#1 . Конечно, компилятор мог бы оптимизировать это во что-нибудь ещё более эффективное по as-if rule, но если запретили весь инлайнинг, то этого не происходит.
За C# замолвлю словечко: тоже создаётся класс с захваченными полями. Для изучения того, как это работает, удобно использовать dnSpy: декомпиляция сборки показывает все "скрытые" элементы, которые сгенерировал компилятор.
рассуждения о IIntConsumer выдают человека, который с языком знаком поверхностно и не знает о std::function )
А вы сами знаете, как std::function устроен?
знаю
это не был "наезд", просто такие вещи бросаются в глаза и не даром в первом комментарий std::function упоминается
Ну если знаете, то расскажите, если не трудно. Не какой у него интерфейс, а как он внутри сохраняет функцию и как вызывает её
по факту там все весьма нетривиально, потому что очень много нюансов (типа того, что тип лямбды никак не определен и является compiler specific) и трюков с производительностью (есть внутренний буфер, который может обсуживать замыкания небольшого размера без "походов" в кучу), но на уровне идеи ничего необычного там нет
очевидно, что все это работает как "виртуальная" функция на метод с заданной сигнатурой
Я немного подумал, и у меня получился такой код, довольно тривиальный (не хватает конструкторов, но в общем оно работает):
код
template<typename RType, typename... Args>
class IFuncHolder {
public:
virtual RType operator()(Args ...args) = 0;
virtual ~IFuncHolder() = default;
};
template<typename RType, typename Callable, typename... Args>
class FuncHolderImpl : public IFuncHolder<RType, Args...> {
public:
FuncHolderImpl(Callable&& callable) : callable(callable) {}
~FuncHolderImpl() {
}
RType operator()(Args ...args) override {
return callable(args...);
}
private:
Callable callable;
};
template<typename Signature>
class my_function;
template<typename RType, typename... Args>
class my_function<RType(Args...)>
{
public:
template<typename Callable>
my_function(Callable&& f) {
func = new FuncHolderImpl<RType, Callable, Args...>(std::forward<Callable>(f));
}
~my_function() {
delete func;
}
RType operator()(Args ...args) {
return func->operator()(args...);
}
private:
IFuncHolder<RType, Args...> *func;
};
void for_each(const vector<int> &vec, my_function<void(int)> func) {
for (const auto &elem: vec) {
func(elem);
}
}
Но в STL написан код без чего-либо виртуального, поэтому непонятно - там написано что-то принципиально другое или примерно то же.
Все примерно так и есть.
ЕМНИП, в опенсорсных реализациях диспетчеризация действительно реализована без виртуальных функций, но она там такая же динамическая, просто написанная вручную.
А вот в оказавшийся под рукой STL из MSVC я даже заглянул: внутри создается объект, реализующий интерфейс _Func_base, то есть в общем-то ровно как у Вас, только чуть более заковыристо и универсально и со Small buffer optimization.
О разнице между лямбдами и обычными функциями и о имплементации лямбд в некторых языках программирования