64

我有一个关于哪种风格更受欢迎的问题:C++0x 中的 std::bind Vs lambda。我知道它们以某种方式服务于不同的目的,但让我们举一个相交功能的例子。

使用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);

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

4

7 回答 7

47

C++0x lambda 是单态的,而 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 时,才会推导出这两个。

于 2011-01-03T02:38:28.857 回答
28

正如您所说, 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
    );
}

这个片段,在纯 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);
    }
 );
}

这读起来还是有点繁重(主要是因为 istream_iterator 业务),但比绑定版本简单多了:)

于 2009-12-19T01:20:12.503 回答
18

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

于 2009-12-18T22:00:56.470 回答
8

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

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

使用 lambda,您可以在 lambda 中添加新逻辑(但如果创建新的可调用对象有意义,则不必强制这样做)。

于 2009-12-19T01:28:30.177 回答
3

我认为这更多的是品味问题。快速掌握新技术或熟悉函数式编程的人可能更喜欢 lambda 语法,而更保守的程序员肯定更喜欢 bind,因为它更接近于传统的 C++ 语法。

这样的决定应该与将使用代码的人协调,可能通过多数票。

然而,这并没有改变事实,lambda 语法更加强大和简洁。

于 2009-12-18T22:15:31.207 回答
2

C++0x lambda 本质上取代了 bind。没有什么是你可以绑定的,你不能重新创建一个简单的包装 lambda 来实现同样的效果。一旦 lambda 支持得到广泛传播,std::tr1::bind 将采用 std::bind1st 等方式。这很好,因为出于某种原因,大多数程序员都很难理解绑定。

于 2009-12-24T20:18:25.423 回答
2

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 个指针,因为它必须存储一个函数指针和 this,再加上至少 1 个元数据值,例如参数的数量。lambda 是 1 指针,因为它指向这个数据,而不是因为它被魔术掉了。

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

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

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

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

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

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

为了说明为什么在这种情况下 lambda 比 bind 好得多,我编写了一些小的测试代码。

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);
}

我使用带有 -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

我们看到,这个 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

我们发现它几乎在所有方面都比 bar 的代码差。baz 本身的代码现在是原来的两倍多,并且包括动态内存分配。

调用者函数不能再内联 foo 甚至直接调用它,相反,它必须经历调用成员函数指针的整个复杂过程。

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

于 2019-11-18T14:49:29.597 回答