Подтвердить что ты не робот

Как представлены и переданы лямбды С++ 11?

Передача лямбда очень проста в С++ 11:

func( []( int arg ) {
  // code
} ) ;

Но мне интересно, какова стоимость передачи лямбды такой функции? Что, если func передает лямбду другим функциям?

void func( function< void (int arg) > f ) {
  doSomethingElse( f ) ;
}

Проходит ли лямбда дорого? Поскольку объект function может быть назначен 0,

function< void (int arg) > f = 0 ; // 0 means "not init" 

это заставляет меня думать, что функции объектов вроде действуют как указатели. Но без использования new, то это означает, что они могут быть похожими на значения struct или классы, которые по умолчанию соответствуют распределению стека и копии по типу.

Как С++ 11 "тело кода" и группа захваченных переменных передаются при передаче объекта функции "по значению"? Есть ли избыточная копия тела кода? Должен ли я отмечать каждый function объект, переданный с помощью const&, чтобы не была сделана копия:

void func( const function< void (int arg) >& f ) {
}

Или объекты-функции каким-то образом проходят иначе, чем обычные структуры С++?

4b9b3361

Ответ 1

Отказ от ответственности: мой ответ несколько упрощен по сравнению с реальностью (я отложил некоторые детали), но большая картина здесь. Кроме того, в Стандарте не указано полностью, как lambdas или std::function должны быть реализованы внутренне (реализация имеет некоторую свободу), поэтому, как и любое обсуждение деталей реализации, ваш компилятор может или не может сделать это именно таким образом.

Но опять-таки это тема, очень похожая на VTables: стандарт не требует многого, но любой разумный компилятор по-прежнему вполне может сделать это таким образом, поэтому я считаю, что стоит немного вникать в это.:)


Лямбда

Самый простой способ реализовать лямбда - это анонимный struct:

auto lambda = [](Args...) -> Return { /*...*/ };

// roughly equivalent to:
struct {
    Return operator ()(Args...) { /*...*/ }
}
lambda; // instance of the anonymous struct

Как и любой другой класс, когда вы передаете свои экземпляры, вам никогда не придется копировать код, а только фактические данные (здесь, вообще ничего).


Объекты, захваченные по значению, копируются в struct:

Value v;
auto lambda = [=](Args...) -> Return { /*... use v, captured by value...*/ };

// roughly equivalent to:
struct Temporary { // note: we can't make it an anonymous struct any more since we need
                   // a constructor, but that just a syntax quirk

    const Value v; // note: capture by value is const by default unless the lambda is mutable
    Temporary(Value v_) : v(v_) {}
    Return operator ()(Args...) { /*... use v, captured by value...*/ }
}
lambda(v); // instance of the struct

Опять же, передача его только означает, что вы передаете данные (v), а не сам код.


Аналогично, объекты, захваченные ссылкой, ссылаются на struct:

Value v;
auto lambda = [&](Args...) -> Return { /*... use v, captured by reference...*/ };

// roughly equivalent to:
struct Temporary {
    Value& v; // note: capture by reference is non-const
    Temporary(Value& v_) : v(v_) {}
    Return operator ()(Args...) { /*... use v, captured by reference...*/ }
}
lambda(v); // instance of the struct

Это почти все, когда речь заходит о самих лямбда (за исключением нескольких деталей реализации, которые я пропустил, но которые не имеют отношения к пониманию того, как это работает).


std::function

std::function является общей оболочкой любого функтора (lambdas, автономные/статические/членные функции, классы-функторы, такие как те, которые я показал,...).

Внутренние элементы std::function довольно сложны, потому что они должны поддерживать все эти случаи. В зависимости от точного типа функтора для этого требуется, по крайней мере, следующие данные (указать или принять детали реализации):

  • Указатель на автономную/статическую функцию.

Или

  • Указатель на копию [см. примечание ниже] функтора (динамически выделенного для того, чтобы разрешить любой тип функтора, как вы его правильно отметили).
  • Указатель на вызываемую функцию-член.
  • Указатель на распределитель, который может копировать функтор и сам (поскольку любой тип функтора можно использовать, указатель-функтор должен быть void* и, следовательно, должен быть такой механизм - возможно, используя полиморфизм, например, базовый класс + виртуальные методы, производный класс создается локально в конструкторах template<class Functor> function(Functor)).

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

Примечание. Я не знаю, где Стандарт обязывает его, но это определенно новая копия, базовый функтор не используется:

int v = 0;
std::function<void()> f = [=]() mutable { std::cout << v++ << std::endl; };
std::function<void()> g = f;

f(); // 0
f(); // 1
g(); // 0
g(); // 1

Итак, когда вы проходите std::function вокруг, он включает по крайней мере те четыре указателя (и действительно на GCC 4.7. 64 бит sizeof(std::function<void()> равно 32, которые представляют собой четыре указателя на 64 бита) и, возможно, динамически выделенную копию функтора ( который, как я уже сказал, содержит только захваченные объекты, вы не копируете код).


Ответьте на вопрос

Какова стоимость передачи лямбда для такой функции? [контекст вопроса: по значению]

Хорошо, как вы можете видеть, это зависит в основном от вашего функтора (либо от функционала struct, либо от lambda), и от переменных, которые он содержит. Накладные расходы по сравнению с непосредственным прохождением функтора struct по величине весьма незначительны, но, конечно, намного выше, чем передача функтора struct по ссылке.

Должен ли я отмечать каждый объект функции, переданный с помощью const&, чтобы не была сделана копия?

Я боюсь, что это очень сложно ответить общим способом. Иногда вам нужно передать ссылку const, иногда по значению, иногда по ссылке rvalue, чтобы вы могли ее переместить. Это действительно зависит от семантики вашего кода.

Правила, по которым вы должны выбрать, - это совершенно другая тема IMO, просто помните, что они такие же, как для любого другого объекта.

В любом случае, теперь у вас есть все ключи для принятия обоснованного решения (опять же, в зависимости от вашего кода и его семантики).

Ответ 2

См. также С++ 11 реализация и модель памяти лямбда

Лямбда-выражение - это просто выражение: выражение. После компиляции он приводит к закрытию объекта во время выполнения.

5.1.2 Лямбда-выражения [expr.prim.lambda]

Оценка лямбда-выражения приводит к временному присвоению (12.2). Этот временной объект называется закрывающим объектом.

Сам объект определяется реализацией и может варьироваться от компилятора к компилятору.

Вот оригинальная реализация лямбда в clang https://github.com/faisalv/clang-glambda

Ответ 3

Если лямбда может быть сделана как простая функция (т.е. она ничего не захватывает), то она делается точно так же. Тем более, что стандарт требует, чтобы он был совместим со старым указателем-указателем к одной и той же сигнатуре. [EDIT: это неточно, см. Обсуждение в комментариях)

В остальном это зависит от реализации, но я бы не стал беспокоиться. Самая простая реализация не делает ничего, кроме информации. Точно так же, как вы просили в захвате. Таким образом, эффект будет таким же, как если бы вы сделали это вручную, создав класс. Или используйте некоторый вариант std:: bind.