绑定兰斯达?

Ara*_*raK 52 c++ lambda functional-programming bind c++11

我有一个关于哪种样式是首选的问题:std :: bind Vs lambda在C++ 0x中.我知道它们服务于某些不同的目的,但我们举一个交叉功能的例子.

使用lambda:

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

使用bind:

uniform_int<> distribution(1, 6);
mt19937 engine;
// bind style
auto dice = bind(distribution, engine);
Run Code Online (Sandbox Code Playgroud)

我们应该选择哪一个?为什么?假设与上述示例相比情况更复杂.即一个优于另一个的优点/缺点是什么?

dv_*_*dv_ 44

C++ 0x lambdas是单态的,而bind可以是多态的.你不能拥有类似的东西

auto f = [](auto a, auto b) { cout << a << ' ' << b; }
f("test", 1.2f);
Run Code Online (Sandbox Code Playgroud)

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"
Run Code Online (Sandbox Code Playgroud)

请注意,此处修复类型A和B. 只有当f实际使用时才会推导出这两个.

  • 在C++ 14中,即使这种差异也会消失 - lambda可以声明为`[](auto x){}` (30认同)
  • @Marcelo Cantos:它显示了两者之间的差异.AraK询问了bind与lambda的差异和优点/缺点. (3认同)
  • @Marcelo Cantos:要证明的声明是"C++ 0x lambdas是单态的",正是因为你_must_用明确类型的参数声明lambda. (2认同)
  • 你这里不需要`bind`.你可以说`auto f = foo();`. (2认同)

Tho*_*tit 26

正如你所说,bind和lambdas并不完全瞄准同一个目标.

例如,对于使用和组合STL算法,lambda是明显的赢家,恕我直言.

为了说明,我记得一个非常有趣的答案,这里是关于堆栈溢出,有人问十六进制魔术数字的想法(如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
    );
}
Run Code Online (Sandbox Code Playgroud)

这个片段在纯C++ 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);
    }
 );
}
Run Code Online (Sandbox Code Playgroud)

这仍然有点沉重(主要是因为istream_iterator业务),但比绑定版本简单得多:)

  • @Beh Tou Cheh我认为如果lambda只包含`return <expression>;`(就像托马斯那样),那么应该推断出类型. (9认同)
  • lambda应该是:[](const std :: string&s) - > bool (4认同)

pra*_*rma 18

C++ 0x lamdba语法比绑定语法更具可读性.一旦你进入超过2-3级绑定,你的代码变得非常难以理解并且难以维护.我更喜欢更直观的lambda语法.

  • 诚然,我认为新的线条可以帮助你的反例.新行对bind没有多大帮助. (13认同)

plu*_*ash 10

lambda 的一个关键优点是它们可以静态引用成员函数,而 bind 只能通过指针引用它们。更糟糕的是,至少在遵循“itanium c++ ABI”(例如 g++ 和 clang++)的编译器中,指向成员函数的指针是普通指针大小的两倍。

因此,至少对于 g++,如果你执行类似的操作,std::bind(&Thing::function, this)你会得到一个大小为三个指针的结果,两个用于指向成员函数的指针,一个用于 this 指针。另一方面,如果你这样做,[this](){function()}你会得到一个只有一个指针大小的结果。

std::function 的 g++ 实现最多可以存储两个指针,无需动态内存分配。因此,将成员函数绑定到 this 并将其存储在 std::function 中将导致动态内存分配,而使用 lambda 并捕获 this 则不会。


来自评论:

成员函数必须至少有 2 个指针,因为它必须存储一个函数指针,并且再加上至少 1 个元数据值(例如参数数量)。lambda 是 1 指针,因为它指向此数据,而不是因为它已被魔法去掉。

“指向成员函数的指针”在大小上是(至少在“itanium C++ ABI”下,但我怀疑其他编译器是类似的)两个指针,因为它存储了指向实际成员函数的指针(或虚拟的虚函数表偏移量)成员函数)以及“this 指针调整”以支持多重继承。将 this 指针绑定到成员成员函数会产生一个大小为三个指针的对象。

另一方面,对于 lambda,每个 lambda 都有一个唯一的类型,并且有关要运行的代码的信息存储为类型的一部分,而不是值的一部分。因此,只有捕获需要存储为 lambda 值的一部分。至少在 g++ 下,按值捕获单个指针的 lambda 具有单个指针的大小。

lambda、指向成员函数的指针或绑定结果都不将参数数量存储为数据的一部分。该信息作为其类型的一部分存储。

std::function 的 g++ 实现有四个指针大小,它由一个指向“调用者”函数的函数指针、一个指向“管理器”函数的函数指针和一个大小为两个指针的数据区域组成。当程序想要调用存储在 std::function 中的可调用对象时,将使用“invoker”函数。当 std::function 中的可调用对象需要复制、销毁等时,将调用管理器函数。

当您构造或分配给 std::function 时,调用程序和管理器函数的实现是通过模板生成的。这就是允许 std::function 存储任意类型的原因。

如果您分配的类型能够适合 std::function 的数据区域,那么 g++ 的实现(我强烈怀疑大多数其他实现)会将其直接存储在那里,因此不需要动态内存分配。

为了演示为什么在这种情况下 lambda 比绑定好得多,我编写了一些小测试代码。

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);
}
Run Code Online (Sandbox Code Playgroud)

我使用带有 -O2 和 -fno-rtti 的“armv7-a clang trunk”选项将其输入 godbolt,并查看了生成的汇编程序。我已经手动分离出了 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
Run Code Online (Sandbox Code Playgroud)

我们看到,bar 本身非常简单,它只是用 this 指针的值以及指向调用者和管理器函数的指针填充 std::function 对象。“invoker”和“manager”函数也非常简单,看不到动态内存分配,并且编译器已将 foo 内联到“invoker”函数中。

现在让我们看看 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
Run Code Online (Sandbox Code Playgroud)

我们发现它几乎在每个方面都比 bar 的代码更糟糕。baz 本身的代码现在长度增加了一倍以上,并且包含动态内存分配。

调用者函数不能再内联 foo 甚至直接调用它,而是必须经历调用成员函数指针的整个繁琐过程。

管理器功能也更加复杂,并且涉及动态内存分配。


R S*_*hko 8

lambdas的一个好处是,当你需要在现有函数之上添加一点逻辑时,它们会更有用.

使用bind,即使只在这一个地方需要逻辑,你也不得不创建一个新的函数/方法/函子.您需要提供一个合适的名称,它可以使代码不易理解,因为它可能会使您分离相关的逻辑.

使用lambda,您可以在lambda中添加新逻辑(但如果创建新的callable有意义,则不强制使用).