Почему лямбда имеет размер 1 байт?

90

Я работаю с памятью некоторых лямбд в 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.

  • Как это возможно?

  • Разве лямбда не должна быть как минимум указателем на ее реализацию?

sdgfsdh
источник
17
он реализован как функциональный объект (a structс an operator())
george_ptr
14
И пустая структура не может иметь размер 0, следовательно, результат 1. Попробуйте что-нибудь запечатлеть и посмотрите, что произойдет с размером.
Мохамад Эльгави
2
Почему лямбда должна быть указателем ??? Это объект, у которого есть оператор вызова.
Керрек С.Б.,
7
Лямбда-выражения в C ++ существуют во время компиляции, а вызовы связаны (или даже встроены) во время компиляции или компоновки. Следовательно, нет необходимости в указателе времени выполнения в самом объекте. @KerrekSB Нет ничего неестественного в предположении, что лямбда-выражение будет содержать указатель на функцию, поскольку большинство языков, реализующих лямбда-выражения, более динамичны, чем C ++.
Кайл Стрэнд
2
@KerrekSB "какое дело" - в каком смысле? Причина объект замыкания может быть пустой (а не содержащим указатель на функцию) является потому , что функция будет называться известна во время компиляции / ссылки. Это то, что OP, кажется, неправильно понял. Я не понимаю, как ваши комментарии проясняют ситуацию.
Кайл Стрэнд

Ответы:

108

Рассматриваемая лямбда фактически не имеет состояния .

Изучите:

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

Целью стандартизации C ++ является программирование высокого уровня с нулевыми накладными расходами на вручную созданный код C.

Теперь, когда вы понимаете, что у вас 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 * нуждаются в некоторых дополнительных битах для удержания смещения в слове). Во-вторых, модель памяти 8086, где void * / int * было сегментом + смещением и могло охватывать всю память, но функции умещались в одном сегменте 64 КБ ( поэтому указатель на функцию был всего 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) . Размер будет расти по мере того, как вы снимаете больше вещей.

(Воспользуйтесь структурной аналогией с долей скептицизма. Хотя это хороший способ рассуждать о том, как лямбда-выражения работают внутри, это не буквальный перевод того, что будет делать компилятор)

ComicSansMS
источник
12

Разве лямбда не должна быть, по крайней мере, указателем на ее реализацию?

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

Тип лямбда-выражения (который также является типом закрывающего объекта) является уникальным, безымянным типом несоединенного класса, называемым закрывающим типом, свойства которого описаны ниже.

[...]

Реализация может определять тип закрытия иначе, чем описано ниже, при условии, что это не изменяет наблюдаемое поведение программы, кроме как путем изменения :

- размер и / или выравнивание типа укупорочного средства ,

- можно ли тривиально копировать тип закрытия (раздел 9),

- является ли тип закрытия классом стандартного макета (раздел 9), или

- является ли тип замыкания классом POD (раздел 9)

В вашем случае - для используемого вами компилятора - вы получаете размер 1, что не означает, что он исправлен. Он может варьироваться в зависимости от реализации компилятора.

legends2k
источник
Вы уверены, что этот бит применим? Лямбда без группы захвата на самом деле не является «закрытием». (В любом случае, стандарт относит лямбды пустой группы захвата к "замыканиям"?)
Кайл Стрэнд
1
Да. Это то, что стандарт гласит: « Оценка лямбда-выражения приводит к временному значению prvalue. Это временное значение называется закрывающим объектом». Захват или нет, это закрывающий объект, только он будет лишен upvalue.
legends2k
Я не голосовал против, но, возможно, тот, кто проголосовал против, не считает этот ответ ценным, потому что он не объясняет, почему возможно (с теоретической точки зрения, а не с точки зрения стандартов) реализовать лямбда-выражения без включения указателя времени выполнения на call-операторская функция. (См. Мое обсуждение с 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.

george_ptr
источник