Ключевым преимуществом лямбда-выражений является то, что они могут ссылаться на функции-члены статически, в то время как 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