#опытным
Сегодня немного проясню ситуацию с лямбдами без захвата.
Повторю предыдущий пост, лямбды без захвата - это объекты с определенным оператором приведения к указателю на функцию. Так как лямбды ничего не захватывают, то их замыкания не содержат полей, поэтому, в целом, для их вызова необязателен объект. Но как именно это реализовано в лямбдах? Сейчас и посмотрим.
Давайте взглянем на простую лямбду:
auto fun = [](int i) { return i*2;};У нее очевидно нет захвата, поэтому она как раз наш абонент. С помощью cppinsights можно посмотреть под капот компилятора и то, во что лямбды и другие сущности превращаются. Примерно вот такую сущность сгенерируют компилятор для этой лямбды:
class __lambda_11_15
{
public:
inline int operator()(int i) const
{
return i * 2;
}
using retType_11_15 = int (*)(int);
inline constexpr operator retType_11_15 () const noexcept
{
return __invoke;
}
private:
static inline /*constexpr */ int __invoke(int i)
{
return __lambda_11_15{}.operator()(i);
}
};
И тут много интересного!
Например, у лямбды без захвата все же генерируется оператор вызова operator().
В добавок к этому определяется оператор приведения к указателю на функцию operator retType_11_15 (), который фактически приводит приватный статический метод класса к указателю. А для переиспользования кода, статический метод на лету конструирует объект и вызывает у него operator().
То есть вот примерно как это работает:
int apply_function(int (*func)(int), int value) {
return func(value); // Вызываем переданную функцию
}
int main() {
auto fun = [](int i) { return i2;};
fun(42);
apply_function(fun, 42);
return 0;
}Здесь у нас 2 вида использования лямбды: через объект замыкания и через коллбэк в apply_function. Посмотрим, что будет вызываться в каждом конкретном случае:
int main()
{
__lambda_11_15 fun = __lambda_11_15{};
fun.operator()(42);
apply_function(fun.operator __lambda_11_15::retType_11_15(), 42);
return 0;
}
В случае вызова через объект замыкания триггерится operator(), а при передаче в другую функцию - оператора приведения к указателю на функцию.
Зачем два способа вызова? Почему нельзя обойтись просто приведением к указателю на функцию?
Вызывать лямбду через указатель на функцию - это лишить себя основной оптимизации компилятора - инлайнинга. Если передавать лямбду, как полноценный тип замыкания, то компилятор будет знать, как встроить код его operator() внутрь callee, потому что все типы определены на этапе компиляции. А по указателю на функцию можно передать все, что угодно. В простых случаях, как в apply_function, может и все хорошо будет. Но в более сложных - вы лишитесь оптимизации.
Но благо указатели на функции используются преимущественно в сишном апи и по-другому просто не получится. В С++ есть шаблоны и объекты и надобность в использовании указателей на функции практически отпадает.
Надеюсь, теперь вы чуть больше о лямбдах знаете)
Know more. Stay cool.
#cppcore #cpp11