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 甚至直接调用它,相反,它必须经历调用成员函数指针的整个复杂过程。
管理器功能也更加复杂,涉及动态内存分配。