Рассматриваемая лямбда фактически не имеет состояния .
Изучите:
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
!
struct
с anoperator()
)