Pracuję z pamięcią niektórych lambd w C ++, ale trochę mnie dziwi ich rozmiar.
Oto mój kod testowy:
#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;
}
Możesz go uruchomić tutaj: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598
Oouptut to:
17
0x7d90ba8f626f
1
Sugeruje to, że wielkość mojej lambdy wynosi 1.
Jak to jest możliwe?
Czy lambda nie powinna być przynajmniej wskaźnikiem jego implementacji?
struct
ze znakiemoperator()
)Odpowiedzi:
Omawiana lambda w rzeczywistości nie ma stanu .
Zbadać:
struct lambda { auto operator()() const { return 17; } };
A gdybyśmy to zrobili
lambda f;
, to jest pusta klasa. Powyższe jest nie tylkolambda
funkcjonalnie podobne do Twojej lambdy, ale (w zasadzie) jest to sposób implementacji Twojej lambdy! (Wymaga również niejawnego rzutowania na operatora wskaźnika funkcji, a nazwalambda
zostanie zastąpiona jakimś pseudo-guidem wygenerowanym przez kompilator)W C ++ obiekty nie są wskaźnikami. To są rzeczywiste rzeczy. Zajmują tylko przestrzeń wymaganą do przechowywania w nich danych. Wskaźnik do obiektu może być większy niż obiekt.
Chociaż możesz myśleć o tej lambdzie jako wskaźniku do funkcji, tak nie jest. Nie możesz ponownie przypisać funkcji
auto f = [](){ return 17; };
do innej funkcji lub lambdy!auto f = [](){ return 17; }; f = [](){ return -42; };
powyższe jest niezgodne z prawem . Nie ma miejsca w
f
do sklepu , która funkcja będzie się nazywać - te informacje są przechowywane w rodzaju dniaf
, a nie w wartościf
!Jeśli zrobiłeś to:
int(*f)() = [](){ return 17; };
albo to:
std::function<int()> f = [](){ return 17; };
nie przechowujesz już bezpośrednio lambdy. W obu tych przypadkach
f = [](){ return -42; }
jest legalny - więc w tych przypadkach przechowujemy w wartości, której funkcji się odwołujemyf
. Isizeof(f)
nie jest już1
, ale raczejsizeof(int(*)())
lub większy (zasadniczo powinien mieć rozmiar wskaźnika lub większy, jak się spodziewasz.std::function
Ma minimalny rozmiar sugerowany przez standard (muszą być w stanie przechowywać „wewnątrz siebie” wywołania do pewnego rozmiaru), które jest co najmniej tak duży jak wskaźnik funkcji w praktyce).W takim
int(*f)()
przypadku przechowujesz wskaźnik funkcji do funkcji, która zachowuje się tak, jakbyś wywołał tę lambdę. Działa to tylko dla lambd bezstanowych (tych z pustą[]
listą przechwytywania).W tym
std::function<int()> f
przypadku tworzyszstd::function<int()>
instancję klasy wymazywania typu, która (w tym przypadku) używa miejsca docelowego new do przechowywania kopii lambda rozmiaru-1 w buforze wewnętrznym (i jeśli przekazano większą lambdę (z większą liczbą stanów ), użyje alokacji sterty).Przypuszczam, że coś takiego jest prawdopodobnie tym, co myślisz. Że lambda to obiekt, którego typ jest opisany przez jego podpis. W C ++ zdecydowano, że lambdy o zerowym koszcie abstrakcji zostaną zastąpione ręczną implementacją obiektu funkcji. Pozwala to przekazać lambdę do
std
algorytmu (lub podobnego) i mieć jej zawartość w pełni widoczną dla kompilatora podczas tworzenia wystąpienia szablonu algorytmu. Gdyby lambda miała podobny typstd::function<void(int)>
, jej zawartość nie byłaby w pełni widoczna, a ręcznie wykonany obiekt funkcyjny mógłby być szybszy.Celem standaryzacji C ++ jest programowanie wysokopoziomowe bez narzutu w stosunku do ręcznie tworzonego kodu C.
Teraz, gdy rozumiesz, że
f
w rzeczywistości jesteś bezpaństwowcem, w twojej głowie powinno pojawić się inne pytanie: lambda nie ma stanu. Dlaczego nie ma rozmiaru0
?Oto krótka odpowiedź.
Wszystkie obiekty w C ++ muszą mieć minimalny rozmiar 1 poniżej standardu, a dwa obiekty tego samego typu nie mogą mieć tego samego adresu. Są one połączone, ponieważ tablica typu
T
będzie miała elementysizeof(T)
rozstawione.Teraz, ponieważ nie ma stanu, czasami nie zajmuje miejsca. Nie może się to zdarzyć, gdy jest „sam”, ale w niektórych sytuacjach może się to zdarzyć.
std::tuple
i podobny kod biblioteki wykorzystuje ten fakt. Oto jak to działa:Ponieważ lambda jest odpowiednikiem klasy z
operator()
przeciążeniem, bezstanowe lambdy (z[]
listą przechwytywania) są pustymi klasami. Mająsizeof
od1
. W rzeczywistości, jeśli odziedziczysz po nich (co jest dozwolone!), Nie zajmą miejsca , o ile nie spowoduje to kolizji adresów tego samego typu . (Jest to znane jako optymalizacja pustej podstawy).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"; } ))
issizeof(int)
(cóż, powyższe jest niedozwolone, ponieważ nie można utworzyć lambdy w nieocenionym kontekście: musisz utworzyć named,auto toy = make_toy(blah);
a następnie zrobićsizeof(blah)
, ale to tylko szum).sizeof([]{std::cout << "hello world!\n"; })
jest nadal1
(podobne kwalifikacje).Jeśli stworzymy inny typ zabawki:
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}; }
ma dwie kopie lambda. Ponieważ nie mogą mieć tego samego adresu,
sizeof(toy2(some_lambda))
jest2
!źródło
()
Dodano dodatkowo .Lambda nie jest wskaźnikiem funkcji.
Lambda to instancja klasy. Twój kod jest w przybliżeniu równoważny z:
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;
Klasa wewnętrzna, która reprezentuje lambdę, nie ma elementów składowych, stąd jej wartość
sizeof()
wynosi 1 (nie może być 0, z powodów podanych w innym miejscu ).Jeśli twoja lambda miałaby wychwycić niektóre zmienne, będą one równoważne członkom klasy, a ty odpowiednio
sizeof()
wskażesz.źródło
sizeof()
podać link „gdzie indziej”, co wyjaśnia, dlaczego nie może wynosić 0?Twój kompilator mniej więcej tłumaczy lambdę na następujący typ struktury:
struct _SomeInternalName { int operator()() { return 17; } }; int main() { _SomeInternalName f; std::cout << f() << std::endl; }
Ponieważ ta struktura nie ma niestatycznych elementów członkowskich, ma taki sam rozmiar jak pusta struktura, czyli
1
.To się zmienia, gdy tylko dodasz niepustą listę przechwytywania do swojej lambdy:
int i = 42; auto f = [i]() { return i; };
Co przełoży się na
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; }
Ponieważ wygenerowana struktura musi teraz przechowywać niestatyczny
int
element członkowski na potrzeby przechwytywania, jej rozmiar wzrośnie dosizeof(int)
. Rozmiar będzie rosnąć w miarę przechwytywania większej liczby rzeczy.(Proszę wziąć analogię do struktury z przymrużeniem oka. Chociaż jest to dobry sposób na uzasadnienie wewnętrznego działania lambd, nie jest to dosłowne tłumaczenie tego, co zrobi kompilator)
źródło
Niekoniecznie. Zgodnie ze standardem rozmiar unikalnej, nienazwanej klasy jest definiowany przez implementację . Fragment z [expr.prim.lambda] , C ++ 14 (moje podkreślenie):
W twoim przypadku - dla używanego kompilatora - otrzymujesz rozmiar 1, co nie oznacza, że został naprawiony. Może się różnić w zależności od różnych implementacji kompilatora.
źródło
Z http://en.cppreference.com/w/cpp/language/lambda :
Z http://en.cppreference.com/w/cpp/language/sizeof
źródło