Бинд против лямбды?

У меня есть вопрос о том, какой стиль предпочтительнее: std::bind Vs lambda в C++0x. Я знаю, что они каким-то образом служат разным целям, но давайте рассмотрим пример пересекающихся функций.

Использование lambda:

uniform_int<> distribution(1, 6);
mt19937 engine;
// lambda style
auto dice = [&]() { return distribution(engine); };

Использование bind:

uniform_int<> distribution(1, 6);
mt19937 engine;
// bind style
auto dice = bind(distribution, engine);

Какой из них мы должны предпочесть? Зачем? предполагая более сложные ситуации по сравнению с упомянутым примером. т. е. каковы преимущества/недостатки одного над другим?


person AraK    schedule 18.12.2009    source источник
comment
есть ли разница в производительности? скорость, потребление памяти, использование кучи?   -  person deft_code    schedule 19.12.2009
comment
@Caspin Я действительно не знаю, есть ли разница в производительности/потреблении памяти между этими двумя инструментами :)   -  person AraK    schedule 19.12.2009
comment
кстати: две версии не эквивалентны, потому что аргументы копий привязки. Альтернатива: bind(ref(distribution),ref(engine))   -  person sellibitze    schedule 19.12.2009


Ответы (7)


Как вы сказали, bind и lambdas не совсем точно нацелены на одну и ту же цель.

Например, для использования и составления алгоритмов STL лямбда-выражения являются явными победителями, ИМХО.

Чтобы проиллюстрировать это, я помню действительно забавный ответ, здесь, на переполнении стека, где кто-то спросил идеи шестнадцатеричных магических чисел (например, 0xDEADBEEF, 0xCAFEBABE, 0xDEADDEAD и т. д.), и ему сказали, что если бы он был настоящим программистом на C++, он бы просто загрузите список английских слов и используйте простой однострочный код C++ :)

#include <iterator>
#include <string>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>

int main()
{
    using namespace boost::lambda;
    std::ifstream ifs("wordsEn.txt");
    std::remove_copy_if(
        std::istream_iterator<std::string>(ifs),
        std::istream_iterator<std::string>(),
        std::ostream_iterator<std::string>(std::cout, "\n"),
        bind(&std::string::size, _1) != 8u
            ||
        bind(
            static_cast<std::string::size_type (std::string::*)(const char*, std::string::size_type) const>(
                &std::string::find_first_not_of
            ),
            _1,
            "abcdef",
            0u
        ) != std::string::npos
    );
}

Этот фрагмент на чистом С++ 98 открывает файл английских слов, сканирует каждое слово и печатает только слова длиной 8 с буквами «a», «b», «c», «d», «e» или «f». письма.

Теперь включите C++0X и lambda:

#include <iterator>
#include <string>
#include <algorithm>
#include <iostream>
#include <fstream>

int main()
{
 std::ifstream ifs("wordsEn.txt");
 std::copy_if(
    std::istream_iterator<std::string>(ifs),
    std::istream_iterator<std::string>(),
    std::ostream_iterator<std::string>(std::cout, "\n"),
    [](const std::string& s)
    {
       return (s.size() == 8 && 
               s.find_first_not_of("abcdef") == std::string::npos);
    }
 );
}

Это все еще немного тяжело читать (в основном из-за бизнеса istream_iterator), но намного проще, чем версия с привязкой :)

person Thomas Petit    schedule 19.12.2009
comment
Хотя эти две части кода не делают одно и то же, я очень четко понял вашу точку зрения :) - person AraK; 19.12.2009
comment
лямбда должна быть: [](const std::string& s) -> bool - person ; 19.12.2009
comment
@Beh Tou Cheh Я думаю, что тип следует вывести, если лямбда состоит только из return <expression>; (как это сделал Томас). - person AraK; 19.12.2009

Лямбда-выражения C++0x мономорфны, тогда как bind может быть полиморфным. Вы не можете иметь что-то вроде

auto f = [](auto a, auto b) { cout << a << ' ' << b; }
f("test", 1.2f);

a и b должны иметь известные типы. С другой стороны, tr1/boost/phoenix/lambda bind позволяет сделать это:

struct foo
{
  typedef void result_type;

  template < typename A, typename B >
  void operator()(A a, B b)
  {
    cout << a << ' ' << b;
  }
};

auto f = bind(foo(), _1, _2);
f("test", 1.2f); // will print "test 1.2"

Обратите внимание, что типы A и B здесь не фиксированы. Эти два будут выведены только тогда, когда f действительно используется.

person dv_    schedule 03.01.2011
comment
Почему бы просто не объявить лямбду с явно заданными параметрами? Это было бы значительным улучшением по сравнению с решением для связывания, показанным выше. Кроме того, если у вас есть более сложная функциональность, которую вы хотите повторно использовать, лямбда все же лучше, чем привязка, поскольку она не требует структуры, даже если вы хотите привязать состояние к функтору: template<...>foo(A a, B b, int state){ cout ... << state; } ... auto f = [](const char* a, float b){ foo(a, b, 42); };. - person Marcelo Cantos; 07.04.2011
comment
@Marcelo Cantos: Утверждение, которое нужно было доказать, заключалось в том, что лямбда-выражения C++0x мономорфны именно потому, что вы должны объявить лямбда-выражение с явно типизированными параметрами. - person MSalters; 07.04.2011
comment
@MSalters: Вопрос был (примерно): что лучше? Я не уверен, как доказательство того, что лямбда-выражения С++ 0x мономорфны, отвечает на вопрос. - person Marcelo Cantos; 07.04.2011
comment
@Marcelo Cantos: это показывает разницу между ними. AraK спросил о различиях и преимуществах/недостатках связывания по сравнению с лямбдой. - person dv_; 08.04.2011
comment
Какая часть вопроса касается различий, а не преимуществ или предпочтений? Даже в названии используется vs, что предполагает соревнование. - person Marcelo Cantos; 08.04.2011
comment
И если вы можете использовать один и тот же f с разными типами аргументов, выдаст ли компилятор ошибку типа? Если да, то оба мономорфны. Просто lambda имеет манифестную типизацию, тогда как bind использует вывод типа. - person Thomas Eding; 11.11.2011
comment
@trinithis: это не дает ошибки типа, поскольку f - это функтор с шаблонным оператором вызова. - person dv_; 30.01.2012
comment
В C++14 даже эта разница исчезнет — лямбда может быть объявлена ​​как [](auto x){}. - person p12; 24.01.2014
comment
Вам не нужен bind здесь. Вы можете просто сказать auto f = foo();. - person Fozi; 31.07.2014

Синтаксис C++ 0x lamdba более удобочитаем, чем синтаксис bind. Как только вы попадаете в привязку более чем на 2-3 уровня, ваш код становится практически нечитаемым и сложным в обслуживании. Я бы предпочел более интуитивно понятный синтаксис лямбда.

person pranavsharma    schedule 18.12.2009
comment
Не согласен. [this](){Type* this_too = this; run([this_too](){this_too->f();});} не читается и не интуитивно понятен. - person Andreas Haferburg; 25.02.2015
comment
По общему признанию, я думаю, что новые строки помогли бы вашему контрпримеру. Новые строки не сильно помогут с привязкой. - person Thomas Eding; 21.07.2016
comment
Во всяком случае, личное мнение. Должен быть комментарий, а не ответ. - person c z; 01.06.2021

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

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

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

person R Samuel Klatchko    schedule 19.12.2009
comment
+1. Мне пришлось закрыть вектор FILE* в дор. Вместо того, чтобы использовать лямбду [](FILE* f) { if(f) fclose(f); }, мне пришлось создать именованную функцию и использовать ее. Функция появилась в private части класса и, таким образом, была отделена многими строками от вызова for_each. - person KitsuneYMG; 19.12.2009

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

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

Однако это не меняет того факта, что лямбда-синтаксис намного мощнее и чище.

person Kornel Kisielewicz    schedule 18.12.2009
comment
Люди в команде постоянно меняются. Читабельность кода очень важна, особенно. для будущих программистов обслуживания. Следовательно, мы должны выбрать любое решение, которое обеспечивает большую читабельность, а между лямбдой и привязкой лямбда определенно выигрывает. - person pranavsharma; 19.12.2009

Лямбда-выражения C++0x по существу заменяют bind. Нет ничего, что вы можете связать, что вы не можете воссоздать тривиальную лямбду-оболочку, чтобы добиться того же. std::tr1::bind пойдет по пути std::bind1st и т. д., как только поддержка лямбда будет широко распространена. Что хорошо, потому что по какой-то причине большинству программистов трудно разобраться с bind.

person Terry Mahaffey    schedule 24.12.2009
comment
Хотя этот ответ не был правильным на момент публикации, он точен для C++14. Ссылка в комментарии выше подтверждает это сейчас. - person Drew Dormann; 24.05.2016

Ключевым преимуществом лямбда-выражений является то, что они могут ссылаться на функции-члены статически, в то время как bind может ссылаться на них только через указатель. Хуже того, по крайней мере, в компиляторах, которые следуют ABI itanium C++ (например, g++ и clang++), указатель на функцию-член в два раза больше обычного указателя.

Так что, по крайней мере, с g++, если вы сделаете что-то вроде std::bind(&Thing::function, this), вы получите результат размером с три указателя, два для указателя на функцию-член и один для указателя this. С другой стороны, если вы выполните [this](){function()}, вы получите результат размером всего в один указатель.

Реализация std::function в g++ может хранить до двух указателей без динамического выделения памяти. Таким образом, привязка функции-члена к этому и сохранение ее в std::function приведет к динамическому выделению памяти при использовании лямбда, а захват этого не произойдет.


Из комментария:

Функция-член должна содержать как минимум 2 указателя, потому что она должна хранить указатель на функцию, а также еще как минимум 1 значение для метаданных, таких как количество аргументов. Лямбда - это 1 указатель, потому что он указывает на эти данные, а не потому, что он был удален волшебным образом.

No

Указатель на функцию-член (по крайней мере, в itanium C++ ABI, но я подозреваю, что другие компиляторы аналогичны) имеют два указателя по размеру, потому что он хранит как указатель на фактическую функцию-член (или смещение vtable для виртуальных функций-членов), так и также корректировка указателя this для поддержки множественного наследования. Привязка указателя this к функции-члену-члену приводит к созданию объекта размером в три указателя.

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

Ни лямбда, ни указатель на функцию-член, ни результат привязки не хранят количество параметров как часть своих данных. Эта информация хранится как часть их типа.

Реализация g++ функции std::function имеет размер четыре указателя, она состоит из указателя функции на вызывающую функцию, указателя функции на функцию менеджера и области данных размером два указателя. Функция вызова используется, когда программа хочет вызвать вызываемый объект, хранящийся в std::function. Функция менеджера вызывается, когда вызываемый объект в std::function необходимо скопировать, уничтожить и т.д.

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

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

Чтобы продемонстрировать, почему в этом случае лямбда намного лучше, чем привязка, я написал небольшой тестовый код.

struct widget
{
    void foo();
    std::function<void()> bar();  
    std::function<void()> baz();  
};

void widget::foo() {
    printf("%p",this);
}

std::function<void()> widget::bar() {
    return [this](){foo();};
}

std::function<void()> widget::baz() {
    return std::bind(&widget::foo, this);
}

Я загрузил это в godbolt, используя опцию armv7-a clang trunk с -O2 и -fno-rtti, и посмотрел на получившийся ассемблер. Я вручную выделил ассемблер для bar и baz. Давайте сначала посмотрим на ассемблер для bar.

widget::bar():
        ldr     r2, .LCPI1_0
        str     r1, [r0]
        ldr     r1, .LCPI1_1
        str     r1, [r0, #8]
        str     r2, [r0, #12]
        bx      lr
.LCPI1_0:
        .long   std::_Function_handler<void (), widget::bar()::$_0>::_M_invoke(std::_Any_data const&)
.LCPI1_1:
        .long   std::_Function_base::_Base_manager<widget::bar()::$_0>::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation)
std::_Function_handler<void (), widget::bar()::$_0>::_M_invoke(std::_Any_data const&):
        ldr     r1, [r0]
        ldr     r0, .LCPI3_0
        b       printf
.LCPI3_0:
        .long   .L.str
std::_Function_base::_Base_manager<widget::bar()::$_0>::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation):
        cmp     r2, #2
        beq     .LBB4_2
        cmp     r2, #1
        streq   r1, [r0]
        mov     r0, #0
        bx      lr
.LBB4_2:
        ldr     r1, [r1]
        str     r1, [r0]
        mov     r0, #0
        bx      lr

Мы видим, что сама панель очень проста, это просто заполнение объекта std::function значением указателя this и указателями на вызывающую и управляющую функции. Функции вызова и менеджера также довольно просты, динамического выделения памяти не видно, а компилятор встроил foo в функцию вызова.

Теперь давайте посмотрим на ассемблер для baz:

widget::baz():
        push    {r4, r5, r6, lr}
        mov     r6, #0
        mov     r5, r0
        mov     r4, r1
        str     r6, [r0, #8]
        mov     r0, #12
        bl      operator new(unsigned int)
        ldr     r1, .LCPI2_0
        str     r4, [r0, #8]
        str     r0, [r5]
        stm     r0, {r1, r6}
        ldr     r1, .LCPI2_1
        ldr     r0, .LCPI2_2
        str     r0, [r5, #8]
        str     r1, [r5, #12]
        pop     {r4, r5, r6, lr}
        bx      lr
.LCPI2_0:
        .long   widget::foo()
.LCPI2_1:
        .long   std::_Function_handler<void (), std::_Bind<void (widget::*(widget*))()> >::_M_invoke(std::_Any_data const&)
.LCPI2_2:
        .long   std::_Function_base::_Base_manager<std::_Bind<void (widget::*(widget*))()> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation)
std::_Function_handler<void (), std::_Bind<void (widget::*(widget*))()> >::_M_invoke(std::_Any_data const&):
        ldr     r0, [r0]
        ldm     r0, {r1, r2}
        ldr     r0, [r0, #8]
        tst     r2, #1
        add     r0, r0, r2, asr #1
        ldrne   r2, [r0]
        ldrne   r1, [r2, r1]
        bx      r1
std::_Function_base::_Base_manager<std::_Bind<void (widget::*(widget*))()> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation):
        push    {r4, r5, r11, lr}
        mov     r4, r0
        cmp     r2, #3
        beq     .LBB6_3
        mov     r5, r1
        cmp     r2, #2
        beq     .LBB6_5
        cmp     r2, #1
        ldreq   r0, [r5]
        streq   r0, [r4]
        b       .LBB6_6
.LBB6_3:
        ldr     r0, [r4]
        cmp     r0, #0
        beq     .LBB6_6
        bl      operator delete(void*)
        b       .LBB6_6
.LBB6_5:
        mov     r0, #12
        bl      operator new(unsigned int)
        ldr     r1, [r5]
        ldm     r1, {r2, r3}
        ldr     r1, [r1, #8]
        str     r0, [r4]
        stm     r0, {r2, r3}
        str     r1, [r0, #8]
.LBB6_6:
        mov     r0, #0
        pop     {r4, r5, r11, lr}
        bx      lr

Мы видим, что он хуже кода для bar почти во всех отношениях. Сам код baz стал вдвое длиннее и включает динамическое выделение памяти.

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

Функция менеджера также существенно сложнее и включает в себя динамическое выделение памяти.

person plugwash    schedule 18.11.2019
comment
Функция-член должна содержать как минимум 2 указателя, потому что она должна хранить указатель на функцию и this, а также как минимум еще 1 значение для метаданных, таких как количество аргументов. Лямбда - это 1 указатель, потому что он указывает на эти данные, а не потому, что он был удален волшебным образом. - person c z; 01.06.2021