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

Я работаю с памятью некоторых lambdas в С++, но я немного озадачен их размером.

Вот мой тестовый код:

#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.

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

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

Ответ 1

Лямбда, о которой идет речь, фактически не имеет состояния.

Проверьте:

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

И если у нас был lambda f;, это пустой класс. Мало того, что выше lambda функционально похож на ваш лямбда, это (в основном), как ваша лямбда реализована! (Он также нуждается в неявном операторе оператора привязки к функциям, а имя lambda будет заменено некоторым псевдо-директором, сгенерированным компилятором)

В С++ объекты не являются указателями. Это настоящие вещи. Они используют только пространство, необходимое для хранения данных в них. Указатель на объект может быть больше объекта.

Хотя вы можете подумать об этой лямбде как о указателе на функцию, это не так. Вы не можете переназначить 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()> типа erasure, который (в этом случае) использует новое размещение для хранения копии lambda size-1 во внутреннем буфере (и, если большая лямбда была передана (с большим количеством состояний), будет использовать распределение кучи).

Как можно предположить, что-то вроде этого, вероятно, то, что вы думаете. То, что лямбда - это объект, тип которого описывается его сигнатурой. В С++ было решено сделать lambdas нулевыми абстракциями над реализацией объекта ручной функции. Это позволяет вам передать лямбда в алгоритм std (или аналогичный) и полностью заполнить его содержимое компилятору при создании шаблона алгоритма. Если у лямбда был тип типа std::function<void(int)>, его содержимое не было бы полностью видимым, а объект функции с ручной обработкой мог бы быть быстрее.

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

Теперь, когда вы понимаете, что ваш f на самом деле без гражданства, в вашей голове должен быть еще один вопрос: у lambda нет состояния. Почему размер не имеет 0?


Существует короткий ответ.

Все объекты в С++ должны иметь минимальный размер 1 под стандартом, а два объекта одного типа не могут иметь один и тот же адрес. Они связаны, потому что массив типа T будет содержать элементы, расположенные sizeof(T).

Теперь, поскольку он не имеет состояния, иногда он может не занимать места. Этого не может быть, если оно "одно", но в некоторых контекстах это может произойти. std::tuple и аналогичный код библиотеки используют этот факт. Вот как это работает:

Поскольку лямбда эквивалентна классу с operator() перегруженными, безстоящие lambdas (с списком захвата []) - все пустые классы. Они имеют sizeof of 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}; }

the sizeof(make_toy( []{std::cout << "hello world!\n"; } )) is 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!

Ответ 2

Лямбда не является указателем на функцию.

Лямбда - это экземпляр класса. Ваш код приблизительно эквивалентен:

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

Ваш компилятор более или менее переводит лямбда на следующий тип структуры:

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). Размер будет расти, поскольку вы захватываете больше вещей.

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

Ответ 4

Не должен ли lambda быть в mimumum указателем на его реализацию?

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

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

[...]

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

- размер и/или выравнивание типа закрытия,

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

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

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

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

Ответ 5

Из http://en.cppreference.com/w/cpp/language/lambda:

Выражение лямбда строит неназначенный временный объект prvalue уникального неназванного не-объединенного типа неагрегатного типа, , известный как тип закрытия, который объявляется (для целей ADL) в самом маленьком блоке область действия, класс или область пространства имен, которая содержит лямбда-выражение.

Если лямбда-выражение захватывает что-либо копией (либо неявно с предложением capture [=], либо явно с захватом, который не включает символ &, например [a, b, c]), тип закрытия включает в себя неназванные нестатические элементы данных, объявленные в неуказанном порядке, которые содержат копии всех объектов, которые были захвачены таким образом.

Для объектов, которые записаны по ссылке (с захватом по умолчанию [&] или при использовании символа &, например [& a, & b, & c]), он не указан, если объявлены дополнительные члены данных в типе замыкания

Из http://en.cppreference.com/w/cpp/language/sizeof

При применении к пустому типу класса всегда возвращается 1.