Gemäß den Quellen, die ich gefunden habe, wird ein Lambda-Ausdruck im Wesentlichen vom Compiler implementiert, der eine Klasse mit überladenem Funktionsaufrufoperator und den referenzierten Variablen als Mitglieder erstellt. Dies deutet darauf hin, dass die Größe der Lambda-Ausdrücke variiert und bei ausreichenden Referenzvariablen die Größe beliebig groß sein kann .
Ein std::function
sollte eine feste Größe haben , aber es muss in der Lage sein, alle Arten von Callables zu verpacken, einschließlich aller Lambdas der gleichen Art. Wie ist es implementiert? Wenn std::function
intern ein Zeiger auf sein Ziel verwendet wird, was passiert dann, wenn die std::function
Instanz kopiert oder verschoben wird? Gibt es Heap-Zuweisungen?
std::function
. Es ist im Wesentlichen eine Handle-Klasse für ein polymorphes Objekt. Eine abgeleitete Klasse der internen Basisklasse wird erstellt, um die auf dem Heap zugewiesenen Parameter zu speichern. Anschließend wird der Zeiger darauf als Unterobjekt von gespeichertstd::function
. Ich glaube, es verwendet Referenzzählungstd::shared_ptr
, um das Kopieren und Verschieben zu handhaben.Antworten:
Die Implementierung von
std::function
kann von Implementierung zu Implementierung unterschiedlich sein, aber die Kernidee ist, dass Typlöschung verwendet wird. Obwohl es mehrere Möglichkeiten gibt, können Sie sich vorstellen, dass eine triviale (nicht optimale) Lösung wie folgt aussehen könnte (std::function<int (double)>
der Einfachheit halber für den speziellen Fall vereinfacht):Bei diesem einfachen Ansatz würde das
function
Objekt nurunique_ptr
einen Basistyp speichern . Für jeden anderen Funktor, der mit dem verwendet wirdfunction
, wird ein neuer Typ erstellt, der von der Basis abgeleitet ist, und ein Objekt dieses Typs wird dynamisch instanziiert. Dasstd::function
Objekt hat immer die gleiche Größe und weist den verschiedenen Funktoren im Heap nach Bedarf Speicherplatz zu.Im wirklichen Leben gibt es verschiedene Optimierungen, die Leistungsvorteile bieten, aber die Antwort erschweren würden. Der Typ könnte kleine Objektoptimierungen verwenden. Der dynamische Versand kann durch einen Zeiger mit freien Funktionen ersetzt werden, der den Funktor als Argument verwendet, um eine Indirektionsebene zu vermeiden. Die Idee ist jedoch im Grunde dieselbe.
In Bezug auf die Frage, wie sich Kopien des
std::function
Verhaltens verhalten, zeigt ein schneller Test an, dass Kopien des internen aufrufbaren Objekts erstellt werden, anstatt den Status zu teilen.Der Test zeigt an, dass
f2
eine Kopie der aufrufbaren Entität anstelle einer Referenz abgerufen wird. Wenn die aufrufbare Entität von den verschiedenenstd::function<>
Objekten gemeinsam genutzt worden wäre, wäre die Ausgabe des Programms 5, 6, 7 gewesen.quelle
std::function
korrekt wäre, wenn das interne Objekt kopiert würde, und ich denke nicht, dass dies der Fall ist (Denken Sie an ein Lambda, das einen Wert erfasst und veränderbar ist und in a gespeichert iststd::function
. Wenn der Funktionsstatus kopiert wurde, kann die Anzahl der Kopienstd::function
in einem Standardalgorithmus zu unterschiedlichen Ergebnissen führen, was unerwünscht ist.std::function
löst die Kopie der Entität eine Zuordnung aus.Die Antwort von @David Rodríguez - dribeas ist gut, um die Typlöschung zu demonstrieren, aber nicht gut genug, da die Typlöschung auch beinhaltet, wie Typen kopiert werden (in dieser Antwort ist das Funktionsobjekt nicht kopierkonstruierbar). Diese Verhaltensweisen werden auch in der gespeichert
function
neben den Funktordaten Objekt gespeichert.Der Trick, der in der STL-Implementierung von Ubuntu 14.04 gcc 4.8 verwendet wird, besteht darin, eine generische Funktion zu schreiben, sie auf jeden möglichen Funktionstyp zu spezialisieren und sie in einen universellen Funktionszeigertyp umzuwandeln. Daher werden die Typinformationen gelöscht .
Ich habe eine vereinfachte Version davon zusammengeschustert. Hoffe es wird helfen
Es gibt auch einige Optimierungen in der STL-Version
construct_f
unddestroy_f
werden in einen Funktionszeiger gemischt (mit einem zusätzlichen Parameter, der angibt, was zu tun ist), um einige Bytes zu sparenunion
, sodass einfunction
Objekt , wenn es aus einem Funktionszeiger erstellt wird, direkt imunion
Heap-Bereich und nicht im Heap-Bereich gespeichert wirdVielleicht ist die STL-Implementierung nicht die beste Lösung, da ich von einer schnelleren Implementierung gehört habe . Ich glaube jedoch, dass der zugrunde liegende Mechanismus der gleiche ist.
quelle
Für bestimmte Arten von Argumenten ("wenn das Ziel von f ein überrufbares Objekt
reference_wrapper
oder ein Funktionszeiger ist") läsststd::function
der Konstruktor keine Ausnahmen zu, sodass die Verwendung des dynamischen Speichers nicht in Frage kommt. In diesem Fall müssen alle Daten direkt im gespeichert werdenstd::function
Objekt .Im allgemeinen Fall (einschließlich des Lambda-Falls) ist die Verwendung des dynamischen Speichers (entweder über den Standard-Allokator oder einen an den
std::function
Konstruktor übergebenen Allokator) nach eigenem Ermessen zulässig. Der Standard empfiehlt, dass Implementierungen keinen dynamischen Speicher verwenden, wenn dies vermieden werden kann. Wie Sie jedoch zu Recht sagenstd::function
, gibt es keine Möglichkeit, dies zu verhindern , wenn das Funktionsobjekt (nicht das Objekt, sondern das darin eingeschlossene Objekt) groß genug ist. dastd::function
hat eine feste größe.Diese Berechtigung zum Auslösen von Ausnahmen wird sowohl dem normalen Konstruktor als auch dem Kopierkonstruktor erteilt, wodurch dynamische Speicherzuweisungen auch während des Kopierens ziemlich explizit möglich sind. Für Bewegungen gibt es keinen Grund, warum ein dynamischer Speicher erforderlich wäre. Der Standard scheint dies nicht explizit zu verbieten und kann es wahrscheinlich nicht, wenn die Verschiebung den Verschiebungskonstruktor des Typs des umschlossenen Objekts aufruft. Sie sollten jedoch davon ausgehen können, dass das Verschieben keine Ursache hat, wenn sowohl die Implementierung als auch Ihre Objekte sinnvoll sind etwaige Zuweisungen.
quelle
Ein
std::function
Überladenoperator()
macht es zu einem Funktorobjekt, Lambdas Arbeit auf die gleiche Weise. Grundsätzlich wird eine Struktur mit Mitgliedsvariablen erstellt, auf die innerhalb deroperator()
Funktion zugegriffen werden kann. Das Grundkonzept ist also, dass ein Lambda ein Objekt (Funktor oder Funktionsobjekt genannt) ist, keine Funktion. Der Standard sagt, keinen dynamischen Speicher zu verwenden, wenn dies vermieden werden kann.quelle
std::function
? Das ist hier die Schlüsselfrage.std::function
Objekt die gleiche Größe hat und nicht die Größe der enthaltenen Lambdas.std::vector<T...>
Objekt eine feste Größe (Copiletime) hat, unabhängig von der tatsächlichen Allokatorinstanz / Anzahl der Elemente.std::function<void ()> f;
keine Notwendigkeit, dort zuzuweisen,std::function<void ()> f = [&]() { /* captures tons of variables */ };
höchstwahrscheinlich zuweisen.std::function<void()> f = &free_function;
wahrscheinlich auch nicht zuweisen ...