Чому лямбда має розмір 1 байт?


89

Я працюю з пам’яттю деяких лямбд у C ++, але трохи здивований їх розміром.

Ось мій тестовий код:

#include <iostream>
#include <string>

int main()
{
  auto f = [](){ return 17; };
  std::cout << f() << std::endl;
  std::cout << &f << std::endl;
  std::cout << sizeof(f) << std::endl;
}

Ви можете запустити тут: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

Вихід:

17
0x7d90ba8f626f
1

Це говорить про те, що розмір моєї лямбди становить 1.

  • Як це можливо?

  • Чи не повинна лямбда, як мінімум, бути вказівником на її реалізацію?


17
його реалізовано як функціональний об'єкт (a structwith an operator())
george_ptr

14
І порожня структура не може мати розмір 0, отже, результат 1. Спробуйте захопити щось і подивіться, що відбувається з розміром.
Мохамад Ельгаві

2
Чому лямбда повинна бути покажчиком ??? Це об’єкт, який має оператор виклику.
Керрек СБ

7
Лямбди в C ++ існують під час компіляції, і виклики пов'язані (або навіть вбудовані) під час компіляції або зв'язку. Тому немає потреби в покажчику часу виконання в самому об’єкті. @KerrekSB Не є неприродним здогадом сподіватися, що лямбда буде містити покажчик функції, оскільки більшість мов, що реалізують лямбди, є більш динамічними, ніж С ++.
Kyle Strand

2
@KerrekSB "що має значення" - в якому сенсі? Причина об'єкт замикання може бути порожньою (а що не містить покажчик на функцію) є тому , що функція буде називатися відома під час компіляції / посилання. Це, здається, ОП неправильно зрозуміло. Я не бачу, як ваші коментарі роз’яснюють речі.
Кайл Стренд,

Відповіді:


107

Лямбда, про яку йде мова, насправді не має стану .

Вивчіть:

struct lambda {
  auto operator()() const { return 17; }
};

А якщо б ми мали lambda f;, це порожній клас. Вищезазначене lambdaфункціонально не лише схоже на вашу лямбду, це (в основному) те, як реалізовано вашу лямбду! (Йому також потрібен неявний привід для функціонування оператора вказівника, і ім'я lambdaбуде замінено на якийсь псевдо- наведений компілятором)

У C ++ об'єкти не є покажчиками. Це справжні речі. Вони використовують лише місце, необхідне для зберігання даних у них. Покажчик на об'єкт може бути більшим за об'єкт.

Хоча ви можете думати про цю лямбду як про вказівник на функцію, це не так. Ви не можете перепризначити auto f = [](){ return 17; };іншу функцію або лямбду!

 auto f = [](){ return 17; };
 f = [](){ return -42; };

вищезазначене є незаконним . Там немає місця в fв магазині , який функція буде називатися - ця інформація зберігається в типі з f, а не у вартості f!

Якщо ви зробили це:

int(*f)() = [](){ return 17; };

або це:

std::function<int()> f = [](){ return 17; };

ви більше не зберігаєте лямбду безпосередньо. В обох випадках випадки f = [](){ return -42; }є законними, тому в цих випадках ми зберігаємо, яку функцію ми використовуємо у значенні f. І sizeof(f)вже не є 1, а скоріше sizeof(int(*)())чи більшим (в основному, має бути розмір покажчика або більший, як ви очікуєте. std::functionМає мінімальний розмір, передбачений стандартом (вони повинні мати можливість зберігати "всередині себе" виклики до певного розміру), які на практиці принаймні настільки велика, як покажчик функції).

У цьому int(*f)()випадку ви зберігаєте вказівник на функцію, яка поводиться так, ніби ви викликали цю лямбду. Це працює лише для лямбд без громадянства (з пустим []списком захоплення).

У цьому std::function<int()> fвипадку ви створюєте std::function<int()>екземпляр класу видалення типу, який (у цьому випадку) використовує розміщення new для зберігання копії лямбда-розміру 1 у внутрішньому буфері (і, якщо була передана більша лямбда (з більшим станом) ), використовував би розподіл купи).

Як припущення, щось подібне, мабуть, саме на вашу думку відбувається. Що лямбда - це об’єкт, тип якого описується його підписом. У C ++ було вирішено зробити абстракції нульових витрат лямбда за реалізацію об'єкта функції вручну. Це дозволяє передавати лямбда-випромінювання в stdалгоритм (або подібний), а його вміст буде повністю видимим компілятору, коли він створює шаблон алгоритму. Якби лямбда мала тип типу std::function<void(int)>, її вміст був би не повністю видимим, а ручно створений функціональний об’єкт міг би бути швидшим.

Метою стандартизації С ++ є програмування високого рівня з нульовими накладними витратами на ручно створений код С.

Тепер, коли ви розумієте, що fнасправді ви без громадянства, у вас в голові повинно виникнути ще одне запитання: лямбда не має стану. Чому він не має розміру 0?


Існує коротка відповідь.

Усі об'єкти в C ++ повинні мати мінімальний розмір 1 за стандартом, а два об'єкти одного типу не можуть мати однакову адресу. Вони пов'язані, оскільки масив типу Tматиме елементи, розташовані sizeof(T)окремо.

Зараз, оскільки він не має стану, іноді він не може займати місця. Це не може статися, коли воно "одне", але в деяких контекстах це може статися. std::tupleі подібний бібліотечний код використовує цей факт. Ось як це працює:

Оскільки лямбда-еквівалент класу з operator()перевантаженими, лямбди без стану (зі []списком захоплення) - це всі порожні класи. Вони мають sizeofв 1. Насправді, якщо ви успадковуєте від них (що дозволено!), Вони не займуть місця, доки це не спричинить зіткнення адрес одного типу . (Це відомо як оптимізація порожньої бази).

template<class T>
struct toy:T {
  toy(toy const&)=default;
  toy(toy &&)=default;
  toy(T const&t):T(t) {}
  toy(T &&t):T(std::move(t)) {}
  int state = 0;
};

template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }

sizeof(make_toy( []{std::cout << "hello world!\n"; } ))є sizeof(int)(ну, вище , є незаконним , тому що ви не можете створити лямбда в не-оцінювали контекст: ви повинні створити іменований auto toy = make_toy(blah);то зробити sizeof(blah), але це просто шум). sizeof([]{std::cout << "hello world!\n"; })все ще 1(схожа кваліфікація).

Якщо ми створимо інший тип іграшки:

template<class T>
struct toy2:T {
  toy2(toy2 const&)=default;
  toy2(T const&t):T(t), t2(t) {}
  T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }

тут є дві копії лямбди. Оскільки вони не можуть поділитися однією адресою, sizeof(toy2(some_lambda))це так 2!


6
Nit: Покажчик функції може бути меншим, ніж void *. Два історичні приклади: По-перше, адресовані машини, де sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * і char * потребує додаткових бітів, щоб утримувати зміщення в межах слова). отже, покажчик функції складав лише 16 бітів).
Мартін Боннер підтримує Моніку

1
@martin правда. ()Додано додатково .
Якк - Адам Неврамонт

50

Лямбда не є покажчиком функції.

Лямбда - це примірник класу. Ваш код приблизно еквівалентний:

class f_lambda {
public:

  auto operator() { return 17; }
};

f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;

Внутрішній клас, який представляє лямбду, не має членів класу, отже, він sizeof()дорівнює 1 (він не може бути 0, з причин, адекватно зазначених в іншому місці ).

Якщо ваша лямбда захопила деякі змінні, вони будуть еквівалентні членам класу, і ви sizeof()вкажете це відповідно.


3
Не могли б ви зробити посилання на "деінде", що пояснює, чому sizeof()не може бути 0?
user1717828

26

Ваш компілятор більш-менш перекладає лямбда-форму до такого типу структури:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}

Оскільки ця структура не має нестатичних членів, вона має такий самий розмір, як і порожня структура, яка є 1.

Це змінюється, як тільки ви додаєте не пустий список захоплення до своєї лямбда-команди:

int i = 42;
auto f = [i]() { return i; };

Що перекладе на

struct _SomeInternalName {
    int i;
    _SomeInternalName(int outer_i) : i(outer_i) {}
    int operator()() { return i; }
};


int main()
{
     int i = 42;
     _SomeInternalName f(i);
     std::cout << f() << std::endl;
}

Оскільки згенерована структура тепер повинна зберігати нестатичний intелемент для захоплення, її розмір зросте до sizeof(int). Розмір буде продовжувати зростати, оскільки ви захоплюєте більше речей.

(Будь ласка, візьміть структурну аналогію із зерном. Хоча це хороший спосіб міркувати про те, як лямбди працюють усередині, це не буквальний переклад того, що буде робити компілятор)


12

Чи не повинна лямбда, як мінімум, бути вказівником на її реалізацію?

Не обов'язково. Відповідно до стандарту розмір унікального, неіменованого класу визначається реалізацією . Витяг із [expr.prim.lambda] , C ++ 14 (курсив мій):

Тип лямбда-виразу (який також є типом об'єкта закриття) - це унікальний, неіменований тип класу непрофспілки - званий тип закриття - властивості якого описані нижче.

[...]

Реалізація може визначати тип закриття інакше, ніж описано нижче, за умови, що це не змінює спостережувану поведінку програми, крім зміни :

- розмір та / або вирівнювання типу закриття ,

- чи тривожно можна скопіювати тип закриття (пункт 9),

- чи є тип закриття стандартним класом розмітки (пункт 9), або

- чи є тип закриття класом POD (пункт 9)

У вашому випадку - для компілятора, який ви використовуєте - ви отримуєте розмір 1, що не означає, що він виправлений. Він може відрізнятися залежно від різних реалізацій компілятора.


Ви впевнені, що цей біт застосовується? Лямбда без групи захоплення насправді не є "закриттям". (Чи стосується цього стандарту лямбди з порожнім захопленням як "закриття"?)
Кайл Стренд,

1
Так. Це те, що в стандарті сказано: " Оцінка лямбда-виразу призводить до тимчасового значення першого значення. Це тимчасове називається об'єктом закриття. ", Захоплюючи чи ні, це об'єкт закриття, просто цей буде позбавлений оцінок.
legends2k

Я не голосував проти, але, можливо, голосуючий не вважає, що ця відповідь цінна, оскільки вона не пояснює, чому можливо (з теоретичної точки зору, а не з точки зору стандартів) реалізовувати лямбди без включення покажчика часу виконання на функція оператора виклику. (Див. Мою дискусію з KerrekSB під цим запитанням.)
Кайл Странд

7

З http://en.cppreference.com/w/cpp/language/lambda :

Вираз лямбда створює неназваний тимчасовий об’єкт prvalue унікального неназваного не об’єднаного неагрегованого типу класу, відомого як тип закриття , який оголошується (для цілей ADL) у найменшій області блоку, області класу або області імен, що містить лямбда-вираз.

Якщо лямбда-вираз захоплює щось за допомогою копії (або неявно з пропозицією захоплення [=], або явно із захопленням, що не включає символ &, наприклад [a, b, c]), тип закриття включає неіменовані нестатичні дані члени , задекларовані в невстановленому порядку, які мають копії всіх об'єктів, які були так захоплені.

Для об’єктів, які фіксуються за допомогою посилання (із захопленням за замовчуванням [&] або при використанні символу &, наприклад [& a, & b, & c]), це якщо додаткові члени даних оголошені в типі закриття

Від http://en.cppreference.com/w/cpp/language/sizeof

При застосуванні до порожнього типу класу завжди повертає 1.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.