Warum hat ein Lambda eine Größe von 1 Byte?

89

Ich arbeite mit der Erinnerung an einige Lambdas in C ++, aber ich bin ein bisschen verwirrt über ihre Größe.

Hier ist mein Testcode:

#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;
}

Sie können es hier ausführen: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

Die Ouptut ist:

17
0x7d90ba8f626f
1

Dies deutet darauf hin, dass die Größe meines Lambda 1 ist.

  • Wie ist das möglich?

  • Sollte das Lambda nicht zumindest ein Hinweis auf seine Umsetzung sein?

sdgfsdh
quelle
17
Es ist als Funktionsobjekt implementiert (a structmit einem operator())
george_ptr
14
Und eine leere Struktur kann nicht die Größe 0 haben, daher das Ergebnis 1. Versuchen Sie, etwas aufzunehmen, und sehen Sie, was mit der Größe passiert.
Mohamad Elghawi
2
Warum sollte ein Lambda ein Zeiger sein ??? Es ist ein Objekt mit einem Anrufer.
Kerrek SB
7
Lambdas in C ++ sind zur Kompilierungszeit vorhanden, und Aufrufe werden zur Kompilierungs- oder Verknüpfungszeit verknüpft (oder sogar inline). Es ist daher kein Laufzeitzeiger im Objekt selbst erforderlich . @KerrekSB Es ist keine unnatürliche Vermutung zu erwarten, dass ein Lambda einen Funktionszeiger enthält, da die meisten Sprachen, die Lambdas implementieren, dynamischer sind als C ++.
Kyle Strand
2
@KerrekSB "worauf kommt es an" - in welchem ​​Sinne? Der Grund, warum ein Abschlussobjekt leer sein kann (anstatt einen Funktionszeiger zu enthalten), liegt darin, dass die aufzurufende Funktion zum Zeitpunkt der Kompilierung / Verknüpfung bekannt ist. Dies scheint das OP missverstanden zu haben. Ich verstehe nicht, wie Ihre Kommentare die Dinge klarstellen.
Kyle Strand

Antworten:

107

Das fragliche Lambda hat eigentlich keinen Staat .

Untersuchen:

struct lambda {
  auto operator()() const { return 17; }
};

Und wenn ja lambda f;, ist es eine leere Klasse. Das Obige lambdaähnelt nicht nur funktional Ihrem Lambda, es ist auch (im Grunde) so, wie Ihr Lambda implementiert wird! (Es wird auch eine implizite Umwandlung in den Funktionszeigeroperator benötigt, und der Name lambdawird durch eine vom Compiler generierte Pseudo-Guid ersetzt.)

In C ++ sind Objekte keine Zeiger. Sie sind tatsächliche Dinge. Sie belegen nur den Speicherplatz, der zum Speichern der Daten in ihnen erforderlich ist. Ein Zeiger auf ein Objekt kann größer als ein Objekt sein.

Während Sie sich dieses Lambda vielleicht als Zeiger auf eine Funktion vorstellen, ist dies nicht der Fall. Sie können das nicht auto f = [](){ return 17; };einer anderen Funktion oder einem anderen Lambda zuweisen!

 auto f = [](){ return 17; };
 f = [](){ return -42; };

Das oben Genannte ist illegal . Es ist kein Platz fzum Speichern der Funktion, die aufgerufen werden soll - diese Informationen werden in der Art von gespeichert f, nicht im Wert von f!

Wenn Sie dies getan haben:

int(*f)() = [](){ return 17; };

oder dieses:

std::function<int()> f = [](){ return 17; };

Sie speichern das Lambda nicht mehr direkt. In beiden Fällen f = [](){ return -42; }ist dies legal. In diesen Fällen speichern wir also, welche Funktion wir im Wert von aufrufen f. Und sizeof(f)ist nicht mehr 1, sondern eher sizeof(int(*)())oder größer (im Grunde genommen Zeigergröße oder größer, wie Sie erwarten. std::functionHat eine vom Standard implizierte Mindestgröße (sie müssen in der Lage sein, Callables bis zu einer bestimmten Größe in sich selbst zu speichern), die ist in der Praxis mindestens so groß wie ein Funktionszeiger).

In diesem int(*f)()Fall speichern Sie einen Funktionszeiger auf eine Funktion, die sich so verhält, als hätten Sie dieses Lambda aufgerufen. Dies funktioniert nur für zustandslose Lambdas (solche mit einer leeren []Erfassungsliste).

In diesem std::function<int()> fFall erstellen Sie eine std::function<int()>Instanz einer Klassenlöschklasse , die (in diesem Fall) die Platzierung new verwendet, um eine Kopie des Lambda der Größe 1 in einem internen Puffer zu speichern (und, wenn ein größeres Lambda übergeben wurde (mit mehr Status) ), würde Heap-Zuordnung verwenden).

Vermutlich ist so etwas wahrscheinlich das, was Sie denken. Dass ein Lambda ein Objekt ist, dessen Typ durch seine Signatur beschrieben wird. In C ++ wurde beschlossen, Lambdas kostengünstige Abstraktionen über die Implementierung des manuellen Funktionsobjekts zu erstellen. Auf diese Weise können Sie ein Lambda an einen stdAlgorithmus (oder einen ähnlichen) übergeben und dessen Inhalt für den Compiler vollständig sichtbar machen, wenn er die Algorithmusvorlage instanziiert. Wenn ein Lambda einen Typ wie hätte std::function<void(int)>, wäre sein Inhalt nicht vollständig sichtbar und ein handgefertigtes Funktionsobjekt könnte schneller sein.

Das Ziel der C ++ - Standardisierung ist die Programmierung auf hoher Ebene ohne Overhead gegenüber handgefertigtem C-Code.

Jetzt, da Sie verstehen, dass Sie ftatsächlich staatenlos sind, sollte es eine andere Frage in Ihrem Kopf geben: Das Lambda hat keinen Zustand. Warum hat es keine Größe 0?


Da ist die kurze Antwort.

Alle Objekte in C ++ müssen unter dem Standard eine Mindestgröße von 1 haben, und zwei Objekte desselben Typs können nicht dieselbe Adresse haben. Diese sind miteinander verbunden, da bei einem Array vom Typ Tdie Elemente sizeof(T)voneinander getrennt sind.

Jetzt, da es keinen Zustand hat, kann es manchmal keinen Platz einnehmen. Dies kann nicht passieren, wenn es "alleine" ist, aber in einigen Kontexten kann es passieren. std::tupleund ähnlicher Bibliothekscode nutzt diese Tatsache aus. So funktioniert es:

Da ein Lambda einer Klasse mit operator()überladenen entspricht, sind zustandslose Lambdas (mit einer []Erfassungsliste) alle leere Klassen. Sie haben sizeofvon 1. Wenn Sie von ihnen erben (was zulässig ist!), Nehmen sie keinen Platz ein , solange dies keine Adresskollision vom gleichen Typ verursacht . (Dies wird als Leerbasisoptimierung bezeichnet.)

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}; }

das sizeof(make_toy( []{std::cout << "hello world!\n"; } ))ist sizeof(int)(nun, das oben Genannte ist illegal, weil Sie kein Lambda in einem nicht bewerteten Kontext erstellen können: Sie müssen ein benanntes auto toy = make_toy(blah);dann erstellen sizeof(blah), aber das ist nur Rauschen). sizeof([]{std::cout << "hello world!\n"; })ist noch 1(ähnliche Qualifikationen).

Wenn wir einen anderen Spielzeugtyp erstellen:

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}; }

Dies hat zwei Kopien des Lambda. Da sie nicht die gleiche Adresse teilen können, sizeof(toy2(some_lambda))ist 2!

Yakk - Adam Nevraumont
quelle
6
Nit: Ein Funktionszeiger kann kleiner sein als eine Leere *. Zwei historische Beispiele: Erstens wortadressierte Maschinen, bei denen sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * und char * benötigen einige zusätzliche Bits, um den Offset innerhalb eines Wortes zu halten.) Zweitens das 8086-Speichermodell, bei dem void * / int * Segment + Offset war und den gesamten Speicher abdecken konnte, aber Funktionen, die in ein einzelnes 64K-Segment passen ( ein Funktionszeiger war also nur 16 Bit).
Martin Bonner unterstützt Monica
1
@martin wahr. Extra ()hinzugefügt.
Yakk - Adam Nevraumont
50

Ein Lambda ist kein Funktionszeiger.

Ein Lambda ist eine Instanz einer Klasse. Ihr Code entspricht ungefähr:

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;

Die interne Klasse, die ein Lambda darstellt, hat keine Klassenmitglieder, daher sizeof()ist sie 1 (sie kann aus an anderer Stelle hinreichend angegebenen Gründen nicht 0 sein ).

Wenn Ihr Lambda einige Variablen erfasst, entsprechen diese den Klassenmitgliedern, und Sie sizeof()geben dies entsprechend an.

Sam Varshavchik
quelle
3
Könnten Sie auf "anderswo" verlinken, was erklärt, warum das sizeof()nicht 0 sein kann?
user1717828
26

Ihr Compiler übersetzt das Lambda mehr oder weniger in den folgenden Strukturtyp:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}

Da diese Struktur keine nicht statischen Elemente hat, hat sie dieselbe Größe wie eine leere Struktur 1.

Das ändert sich, sobald Sie Ihrem Lambda eine nicht leere Erfassungsliste hinzufügen:

int i = 42;
auto f = [i]() { return i; };

Welches wird zu übersetzen

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;
}

Da die generierte Struktur jetzt ein nicht statisches intElement für die Erfassung speichern muss, wächst ihre Größe auf sizeof(int). Die Größe wächst weiter, wenn Sie mehr Material aufnehmen.

(Bitte nehmen Sie die Strukturanalogie mit einem Körnchen Salz. Es ist zwar eine gute Möglichkeit, darüber nachzudenken, wie Lambdas intern funktionieren, dies ist jedoch keine wörtliche Übersetzung dessen, was der Compiler tun wird.)

ComicSansMS
quelle
12

Sollte das Lambda nicht zumindest ein Hinweis auf seine Umsetzung sein?

Nicht unbedingt. Gemäß dem Standard ist die Größe der eindeutigen, unbenannten Klasse implementierungsdefiniert . Auszug aus [expr.prim.lambda] , C ++ 14 (Schwerpunkt Mine):

Der Typ des Lambda-Ausdrucks (der auch der Typ des Abschlussobjekts ist) ist ein eindeutiger, unbenannter Nicht-Vereinigungsklassentyp - der als Schließungstyp bezeichnet wird - dessen Eigenschaften im Folgenden beschrieben werden.

[...]

Eine Implementierung kann den Schließungstyp anders definieren als nachstehend beschrieben, vorausgesetzt, dies ändert das beobachtbare Verhalten des Programms nur durch Ändern von :

- die Größe und / oder Ausrichtung des Verschlusstyps ,

- ob der Verschlusstyp trivial kopierbar ist (Abschnitt 9),

- ob der Verschlusstyp eine Standardlayoutklasse ist (Abschnitt 9) oder

- ob der Verschlusstyp eine POD-Klasse ist (Abschnitt 9)

In Ihrem Fall - für den von Ihnen verwendeten Compiler - erhalten Sie eine Größe von 1, was nicht bedeutet, dass er behoben ist. Sie kann zwischen verschiedenen Compiler-Implementierungen variieren.

legends2k
quelle
Sind Sie sicher, dass dieses Bit zutrifft? Ein Lambda ohne Fanggruppe ist nicht wirklich eine "Schließung". (Bezieht sich der Standard ohnehin auf Lambdas mit leeren Fanggruppen als "Verschlüsse"?)
Kyle Strand
1
Ja tut es. Dies ist, was der Standard sagt: " Die Auswertung eines Lambda-Ausdrucks führt zu einem temporären Wert. Dieser temporäre Wert wird als Abschlussobjekt bezeichnet. " Ob erfasst oder nicht, es ist ein Abschlussobjekt, nur dass es keine Upvalues ​​gibt.
Legends2k
Ich habe nicht herabgestimmt, aber möglicherweise hält der Downvoter diese Antwort nicht für wertvoll, da sie nicht erklärt, warum es möglich ist (aus theoretischer Sicht, nicht aus Standardsicht), Lambdas zu implementieren, ohne einen Laufzeitzeiger auf das einzuschließen Anruferfunktion. (Siehe meine Diskussion mit KerrekSB unter der Frage.)
Kyle Strand
7

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

Der Lambda-Ausdruck erstellt ein unbenanntes temporäres prvalue-Objekt des eindeutigen unbenannten nicht gewerkschaftlich organisierten nicht aggregierten Klassentyps, der als Closure-Typ bezeichnet wird und (für die Zwecke von ADL) im kleinsten Blockbereich, Klassenbereich oder Namespace-Bereich deklariert wird der Lambda-Ausdruck.

Wenn der Lambda-Ausdruck etwas durch Kopie erfasst (entweder implizit mit der Erfassungsklausel [=] oder explizit mit einer Erfassung, die das Zeichen & nicht enthält, z. B. [a, b, c]), enthält der Schließungstyp unbenannte nicht statische Daten Mitglieder , die in nicht spezifizierter Reihenfolge deklariert sind und Kopien aller Entitäten enthalten, die auf diese Weise erfasst wurden.

Für die Entitäten, die als Referenz erfasst werden (mit der Standarderfassung [&] oder bei Verwendung des Zeichens &, z. B. [& a, & b, & c]), ist nicht angegeben, ob zusätzliche Datenelemente im Schließungstyp deklariert sind

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

Bei Anwendung auf einen leeren Klassentyp wird immer 1 zurückgegeben.

george_ptr
quelle