【问题标题】:Bind Vs Lambda?绑定与 Lambda?
【发布时间】:2010-12-28 04:53:33
【问题描述】:

我有一个关于哪种风格更受欢迎的问题: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);

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

【问题讨论】:

  • 有性能差异吗?速度、内存消耗、堆使用情况?
  • @Caspin 我真的不知道这两个工具在性能/内存消耗上是否存在差异:)
  • 顺便说一句:这两个版本不等效,因为绑定复制参数。替代方案:bind(ref(distribution),ref(engine))

标签: c++ functional-programming lambda c++11 bind


【解决方案1】:

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 时才会推导出这两个。

【讨论】:

  • @Marcelo Cantos:要证明的语句是“C++0x lambda 是单态的”,正是因为您必须使用显式类型的参数声明 lambda。跨度>
  • @Marcelo Cantos:它显示了两者之间的差异。 AraK 询问了 bind 与 lambda 的区别和优缺点。
  • @trinithis:它不会给出类型错误,因为 f 是一个带有模板化调用运算符的函子。
  • 在 C++14 中,即使这种差异也会消失——一个 lambda 可以声明为 [](auto x){}
  • 这里不需要bind。你可以直接说auto f = foo();
【解决方案2】:

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

这个sn-p,纯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这个业务),但是比bind版本简单很多:)

【讨论】:

  • 虽然两段代码做的不是一回事,但我很清楚你的观点:)
  • lambda 应该是:[](const std::string& s) -> bool
  • @Beh Tou Cheh 我认为如果 lambda 仅由 return &lt;expression&gt;; 组成(就像 Thomas 所做的那样),则应该推断出类型。
【解决方案3】:

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

【讨论】:

  • 不同意。 [this](){Type* this_too = this; run([this_too](){this_too-&gt;f();});} 既不可读也不直观。
  • 诚然,我认为新行会帮助您的反例。新行对绑定没有多大帮助。
  • 个人意见。应该是评论,而不是答案。
【解决方案4】:

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

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

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

【讨论】:

  • +1。我不得不在 dtor 中关闭 FILE* 的向量。我不得不创建一个命名函数并使用它,而不是能够使用 lambda [](FILE* f) { if(f) fclose(f); }。该函数出现在类的private 部分中,因此与for_each 调用被许多行分隔
【解决方案5】:

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

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

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

【讨论】:

  • 团队中的人不断变化。代码可读性非常重要,尤其是。对于未来的维护程序员。因此,我们应该选择任何提供更多可读性的解决方案,并且在 lamdba 和 bind 之间,lamda 绝对是最重要的。
【解决方案6】:

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

【讨论】:

  • 虽然这个答案在发布时并不正确,但它对于 C++14 是准确的。上面评论中的链接现在证实了这一点。
【解决方案7】:

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

因此,至少对于 g++,如果您执行std::bind(&amp;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 本身的代码现在是原来的两倍多,并且包括动态内存分配。

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

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

【讨论】:

  • 一个成员函数必须至少有 2 个指针,因为它必须存储一个函数指针和this,再加上至少 1 个元数据值,例如参数的数量。 lambda 是 1 指针,因为它指向这个数据,而不是因为它被魔术掉了。
猜你喜欢
  • 1970-01-01
  • 2016-04-02
  • 2015-02-06
  • 2021-03-17
  • 1970-01-01
  • 1970-01-01
  • 2021-12-14
  • 2013-06-06
相关资源
最近更新 更多