Я работаю с памятью некоторых лямбд в 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.
Как это возможно?
Разве лямбда не должна быть как минимум указателем на ее реализацию?
struct
с anoperator()
)Ответы:
Рассматриваемая лямбда фактически не имеет состояния .
Изучите:
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
!источник
()
Добавлено доп .Лямбда - это не указатель на функцию.
Лямбда - это экземпляр класса. Ваш код примерно эквивалентен:
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()
соответственно.источник
sizeof()
не может быть 0?Ваш компилятор более или менее переводит лямбда в следующий тип структуры:
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)
. Размер будет расти по мере того, как вы снимаете больше вещей.(Воспользуйтесь структурной аналогией с долей скептицизма. Хотя это хороший способ рассуждать о том, как лямбда-выражения работают внутри, это не буквальный перевод того, что будет делать компилятор)
источник
Не обязательно. Согласно стандарту размер уникального безымянного класса определяется реализацией . Выдержка из [expr.prim.lambda] , C ++ 14 (выделено мной):
В вашем случае - для используемого вами компилятора - вы получаете размер 1, что не означает, что он исправлен. Он может варьироваться в зависимости от реализации компилятора.
источник
С http://en.cppreference.com/w/cpp/language/lambda :
Из http://en.cppreference.com/w/cpp/language/sizeof
источник