Как стать автором
Обновить

Комментарии 41

И во-вторых, лямбда функция, захватывающая аргументы снаружи, может быть передана только в шаблонную функцию, у которой тип принимаемой функции является шаблонным параметром

Вы можете использовать обёртку в виде std::function<void(int)>.

спасибо. Я постараюсь дополнить статью с учётом этого.

НЛО прилетело и опубликовало эту надпись здесь

и почти гарантированную аллокацию памяти

зависит от того, насколько много добра вы ходите захватить

во всех известных мне компиляторах std::function имеет внутри себя буфер для того, чтобы в случае небольшого контекста не бежать сразу в кучу

function ref никогда не требует аллокации по очевидным причинам. А function почти никогда не будет аллоцировать лямбду, только если она какая то громадная или бросает исключение в муве или алигмент у неё какой то кривой, этого почти никогда не бывает.

Получше разберитесь в вопросе

НЛО прилетело и опубликовало эту надпись здесь

в function ref нет никакой SOO и быть не может

НЛО прилетело и опубликовало эту надпись здесь

Ты несёшь абсолютный бред, ничего не мешает хранить сам указатель на функцию, для этого никогда не нужно ни аллокации ни SOO оптимизаций. SOO есть как раз в std::function и подобных объектах, которые ВЛАДЕЮТ сущностью.

НЛО прилетело и опубликовало эту надпись здесь

Вообще-то это замыкания, а не лямбда-функции.

Замыкание — это свойство анонимной функции захватывать переменные из контекста.

Не обязательно анонимной.

Да, в некоторых языках может быть и такое.

Мне кажется, столь фундаментальные концепции не должны зависеть от ограничений отдельных языков.

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

Идея анонимной функции ничем не отличается от анонимного экземпляра, скажем, структуры.

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

Что скажете про 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.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории