Dlaczego lambda ma rozmiar 1 bajtu?

90

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?

sdgfsdh
źródło
17
jest zaimplementowany jako obiekt funkcji (a structze znakiem operator())
george_ptr
14
Pusta struktura nie może mieć rozmiaru 0, stąd wynik 1. Spróbuj coś uchwycić i zobacz, co się stanie z rozmiarem.
Mohamad Elghawi
2
Dlaczego lambda ma być wskaźnikiem ??? To obiekt, który ma operatora połączeń.
Kerrek SB
7
Lambdy w C ++ istnieją w czasie kompilacji, a wywołania są łączone (lub nawet wstawiane) w czasie kompilacji lub łączenia. Dlatego nie ma potrzeby umieszczania wskaźnika czasu wykonywania w samym obiekcie. @KerrekSB Nie jest nienaturalnym przypuszczeniem, że lambda będzie zawierać wskaźnik funkcji, ponieważ większość języków implementujących lambdy jest bardziej dynamiczna niż C ++.
Kyle Strand
2
@KerrekSB "co się liczy" - w jakim sensie? Powodem obiekt zamknięcie może być pusta (zamiast zawierający wskaźnik funkcji) jest dlatego funkcja być nazywany jest znany w czasie kompilacji / link. Wydaje się, że właśnie to PO źle zrozumiał. Nie rozumiem, jak twoje komentarze wyjaśniają pewne sprawy.
Kyle Strand

Odpowiedzi:

108

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 tylko lambdafunkcjonalnie 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 nazwa lambdazostanie 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 fdo sklepu , która funkcja będzie się nazywać - te informacje są przechowywane w rodzaju dnia f, a nie w wartości f!

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łujemy f. I sizeof(f)nie jest już 1, ale raczej sizeof(int(*)())lub większy (zasadniczo powinien mieć rozmiar wskaźnika lub większy, jak się spodziewasz. std::functionMa 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()> fprzypadku tworzysz std::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 stdalgorytmu (lub podobnego) i mieć jej zawartość w pełni widoczną dla kompilatora podczas tworzenia wystąpienia szablonu algorytmu. Gdyby lambda miała podobny typ std::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 fw rzeczywistości jesteś bezpaństwowcem, w twojej głowie powinno pojawić się inne pytanie: lambda nie ma stanu. Dlaczego nie ma rozmiaru 0?


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 Tbędzie miała elementy sizeof(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::tuplei 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ą sizeofod 1. 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"; } ))is sizeof(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 nadal 1(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))jest 2!

Yakk - Adam Nevraumont
źródło
6
Nit: wskaźnik funkcji może być mniejszy niż void *. Dwa historyczne przykłady: Po pierwsze maszyny adresowane słowem, gdzie sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * i char * wymagają dodatkowych bitów, aby utrzymać przesunięcie w słowie) Po drugie model pamięci 8086, w którym void * / int * był segment + offset i mógł pokryć całą pamięć, ale funkcje mieszczące się w pojedynczym segmencie 64K ( więc wskaźnik funkcji miał tylko 16 bitów).
Martin Bonner wspiera Monikę
1
@martin true. ()Dodano dodatkowo .
Yakk - Adam Nevraumont
50

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.

Sam Varshavchik
źródło
3
Czy możesz sizeof()podać link „gdzie indziej”, co wyjaśnia, dlaczego nie może wynosić 0?
user1717828
26

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 intelement członkowski na potrzeby przechwytywania, jej rozmiar wzrośnie do sizeof(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)

ComicSansMS
źródło
12

Czy lambda nie powinna być przy mimumum wskaźnikiem do jego implementacji?

Niekoniecznie. Zgodnie ze standardem rozmiar unikalnej, nienazwanej klasy jest definiowany przez implementację . Fragment z [expr.prim.lambda] , C ++ 14 (moje podkreślenie):

Typ wyrażenia lambda (będącego również typem obiektu zamknięcia) jest unikalnym, nienazwanym typem klasy nieunionowej - nazywanym typem zamknięcia - którego właściwości opisano poniżej.

[…]

Implementacja może definiować typ zamknięcia inaczej niż opisano poniżej, pod warunkiem że nie zmienia to obserwowalnego zachowania programu poza zmianą :

- rozmiar i / lub ułożenie typu zamknięcia ,

- czy typ zamknięcia jest trywialnie kopiowalny (punkt 9),

- czy typ zamknięcia jest standardową klasą układu (klauzula 9), lub

- czy typ zamknięcia jest klasą POD (klauzula 9)

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.

legends2k
źródło
Czy na pewno ten fragment ma zastosowanie? Lambda bez grupy przechwytywania nie jest tak naprawdę „zamknięciem”. (Czy norma odnosi się do lambd pustych grup przechwytywania jako „zamknięcia”?)
Kyle Strand
1
Tak. To jest to, co mówi standard: " Ocena wyrażenia lambda skutkuje tymczasową wartością pr. Ten obiekt tymczasowy nazywa się obiektem zamknięcia. ", Przechwytując lub nie, jest to obiekt zamknięcia, tylko że będzie pozbawiony podwyższonych wartości.
legends2k
Nie głosowałem przeciw, ale prawdopodobnie przeciwnik uważa, że ​​ta odpowiedź nie jest wartościowa, ponieważ nie wyjaśnia, dlaczego jest możliwe (z perspektywy teoretycznej, a nie standardowej) zaimplementowanie lambd bez uwzględnienia wskaźnika czasu wykonywania funkcja operatora połączeń. (Zobacz moją dyskusję z KerrekSB pod pytaniem.)
Kyle Strand
7

Z http://en.cppreference.com/w/cpp/language/lambda :

Wyrażenie lambda konstruuje nienazwany obiekt tymczasowy wartości prvalue o unikalnym, nienazwanym, niezagregowanym typie klasy, zwanym typem zamknięcia , który jest zadeklarowany (na potrzeby ADL) w najmniejszym zakresie blokowym, zakresie klas lub zakresu nazw, który zawiera wyrażenie lambda.

Jeśli wyrażenie lambda przechwytuje cokolwiek przez kopię (niejawnie z klauzulą ​​przechwytywania [=] lub jawnie z przechwyceniem, które nie zawiera znaku &, np. [A, b, c]), typ zamknięcia obejmuje nienazwane dane niestatyczne członkowie , zadeklarowani w nieokreślonej kolejności, przechowujący kopie wszystkich bytów, które zostały w ten sposób schwytane.

W przypadku jednostek, które są przechwytywane przez odniesienie (z domyślnym przechwytywaniem [&] lub w przypadku używania znaku &, np. [& A, & b, & c]) nie jest określone, czy w typie zamknięcia zadeklarowano dodatkowe elementy danych

Z http://en.cppreference.com/w/cpp/language/sizeof

Po zastosowaniu do pustego typu klasy zawsze zwraca 1.

george_ptr
źródło