Ответ 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
!