Wann und wie verwenden Sie in C ++ eine Rückruffunktion?
EDIT:
Ich würde gerne ein einfaches Beispiel sehen, um eine Rückruffunktion zu schreiben.
Wann und wie verwenden Sie in C ++ eine Rückruffunktion?
EDIT:
Ich würde gerne ein einfaches Beispiel sehen, um eine Rückruffunktion zu schreiben.
Antworten:
Hinweis: Die meisten Antworten beziehen sich auf Funktionszeiger. Dies ist eine Möglichkeit, eine "Rückruf" -Logik in C ++ zu erreichen, aber derzeit nicht die günstigste, die ich denke.
Was sind Rückrufe (?) Und warum werden sie verwendet (!)
Ein Rückruf ist ein Aufruf (siehe weiter unten), der von einer Klasse oder Funktion akzeptiert wird und zum Anpassen der aktuellen Logik in Abhängigkeit von diesem Rückruf verwendet wird.
Ein Grund für die Verwendung von Rückrufen ist das Schreiben von Generika Code , der von der Logik in der aufgerufenen Funktion unabhängig ist und mit verschiedenen Rückrufen wiederverwendet werden kann.
Viele Funktionen der Standardalgorithmusbibliothek
<algorithm>
verwenden Rückrufe. Beispielsweisefor_each
wendet der Algorithmus einen unären Rückruf auf jedes Element in einer Reihe von Iteratoren an:Dies kann verwendet werden, um zuerst einen Vektor zu erhöhen und dann zu drucken, indem entsprechende Callables übergeben werden, zum Beispiel:
welche druckt
Eine andere Anwendung von Rückrufen ist die Benachrichtigung von Anrufern über bestimmte Ereignisse, die ein gewisses Maß an Flexibilität bei der statischen / Kompilierungszeit ermöglicht.
Persönlich verwende ich eine lokale Optimierungsbibliothek, die zwei verschiedene Rückrufe verwendet:
Somit ist der Bibliotheksdesigner nicht dafür verantwortlich zu entscheiden, was mit den Informationen geschieht, die dem Programmierer über den Benachrichtigungsrückruf gegeben werden, und er muss sich keine Gedanken darüber machen, wie Funktionswerte tatsächlich bestimmt werden, da sie durch den logischen Rückruf bereitgestellt werden. Diese Dinge richtig zu machen, ist eine Aufgabe des Bibliotheksbenutzers und hält die Bibliothek schlank und allgemeiner.
Darüber hinaus können Rückrufe ein dynamisches Laufzeitverhalten ermöglichen.
Stellen Sie sich eine Art Game-Engine-Klasse vor, die eine Funktion hat, die jedes Mal ausgelöst wird, wenn der Benutzer eine Taste auf seiner Tastatur drückt, sowie eine Reihe von Funktionen, die Ihr Spielverhalten steuern. Mit Rückrufen können Sie zur Laufzeit (erneut) entscheiden, welche Aktion ausgeführt wird.
Hier verwendet die Funktion
key_pressed
die darin gespeicherten Rückrufeactions
, um das gewünschte Verhalten zu erhalten, wenn eine bestimmte Taste gedrückt wird. Wenn der Spieler die Schaltfläche zum Springen ändert, kann der Motor anrufenund ändern Sie so das Verhalten eines Anrufs auf
key_pressed
(was die Anrufeplayer_jump
), sobald diese Taste das nächste Mal im Spiel gedrückt wird.Was sind Callables in C ++ (11)?
Eine formellere Beschreibung finden Sie unter C ++ - Konzepte: Auf cppreference aufrufbar.
Die Rückruffunktionalität kann in C ++ (11) auf verschiedene Arten realisiert werden, da sich verschiedene Dinge als aufrufbar herausstellen * :
std::function
Objekteoperator()
)* Hinweis: Zeiger auf Datenelemente können ebenfalls aufgerufen werden, es wird jedoch überhaupt keine Funktion aufgerufen.
Mehrere wichtige Möglichkeiten, um Rückrufe im Detail zu schreiben
Hinweis: Ab C ++ 17 kann ein Aufruf wie
f(...)
geschrieben werden,std::invoke(f, ...)
der auch den Zeiger auf den Elementfall behandelt.1. Funktionszeiger
Ein Funktionszeiger ist der 'einfachste' (in Bezug auf die Allgemeinheit; in Bezug auf die Lesbarkeit wohl der schlechteste) Typ, den ein Rückruf haben kann.
Lassen Sie uns eine einfache Funktion haben
foo
:1.1 Schreiben einer Funktionszeiger- / Typnotation
Ein Funktionszeigertyp hat die Notation
wo ein benannter Funktionszeigertyp aussehen wird
Die
using
Erklärung gibt uns die Möglichkeit, die Dinge ein wenig lesbarer zu machen, da dastypedef
forf_int_t
auch wie folgt geschrieben werden kann:Wo (zumindest für mich) klarer ist, dass
f_int_t
es sich um den neuen Typalias handelt, ist die Erkennung des Funktionszeigertyps ebenfalls einfacherUnd eine Deklaration einer Funktion unter Verwendung eines Rückrufs vom Funktionszeigertyp lautet :
1.2 Rückrufnotation
Die Aufrufnotation folgt der einfachen Funktionsaufrufsyntax:
1.3 Callback-Notation und kompatible Typen
Eine Rückruffunktion, die einen Funktionszeiger verwendet, kann unter Verwendung von Funktionszeigern aufgerufen werden.
Die Verwendung einer Funktion, die einen Funktionszeiger-Rückruf akzeptiert, ist ziemlich einfach:
1.4 Beispiel
Es kann eine Funktion geschrieben werden, die nicht davon abhängt, wie der Rückruf funktioniert:
wo möglich könnten Rückrufe sein
verwendet wie
2. Zeiger auf Mitgliedsfunktion
Ein Zeiger auf eine Elementfunktion (einer Klasse
C
) ist ein spezieller Typ eines (und noch komplexeren) Funktionszeigers, für dessen Bearbeitung ein Objekt vom Typ erforderlich istC
.2.1 Zeiger auf Elementnotation / Typnotation schreiben
Ein Zeiger auf den Elementfunktionstyp für eine Klasse
T
hat die NotationDabei sieht ein benannter Zeiger auf die Elementfunktion - analog zum Funktionszeiger - folgendermaßen aus:
Beispiel: Deklarieren einer Funktion, die einen Zeiger auf den Rückruf einer Mitgliedsfunktion als eines ihrer Argumente verwendet:
2.2 Rückrufnotation
Der Zeiger auf die Elementfunktion von
C
kann in Bezug auf ein Objekt vom Typ aufgerufen werden,C
indem Elementzugriffsoperationen für den dereferenzierten Zeiger verwendet werden. Hinweis: Klammern erforderlich!Hinweis: Wenn ein Zeiger auf
C
verfügbar ist , ist die Syntax äquivalent (wobei der Zeiger auf ebenfallsC
dereferenziert werden muss):2.3 Callback-Notation und kompatible Typen
Eine Rückruffunktion, die einen Elementfunktionszeiger der Klasse verwendet,
T
kann unter Verwendung eines Elementfunktionszeigers der Klasse aufgerufen werdenT
.Die Verwendung einer Funktion, die einen Zeiger auf den Rückruf von Elementfunktionen verwendet, ist - analog zu Funktionszeigern - ebenfalls recht einfach:
3.
std::function
Objekte (Header<functional>
)Die
std::function
Klasse ist ein polymorpher Funktions-Wrapper zum Speichern, Kopieren oder Aufrufen von Callables.3.1 Schreiben einer
std::function
Objekt- / TypnotationDer Typ eines
std::function
Objekts, in dem ein aufrufbarer Objekt gespeichert ist, sieht folgendermaßen aus:3.2 Rückrufnotation
Die Klasse
std::function
hatoperator()
definiert, mit welcher ihr Ziel aufgerufen werden kann.3.3 Callback-Notation und kompatible Typen
Der
std::function
Rückruf ist allgemeiner als Funktionszeiger oder Zeiger auf Elementfunktionen, da verschiedene Typen übergeben und implizit in einstd::function
Objekt konvertiert werden können .3.3.1 Funktionszeiger und Zeiger auf Elementfunktionen
Ein Funktionszeiger
oder ein Zeiger auf die Mitgliedsfunktion
kann verwendet werden.
3.3.2 Lambda-Ausdrücke
Ein unbenannter Abschluss eines Lambda-Ausdrucks kann in einem
std::function
Objekt gespeichert werden:3.3.3
std::bind
AusdrückeDas Ergebnis eines
std::bind
Ausdrucks kann übergeben werden. Zum Beispiel durch Binden von Parametern an einen Funktionszeigeraufruf:Wobei auch Objekte als Objekt für den Aufruf des Zeigers auf Elementfunktionen gebunden werden können:
3.3.4 Funktionsobjekte
Objekte von Klassen mit einer ordnungsgemäßen
operator()
Überladung können ebenfalls in einemstd::function
Objekt gespeichert werden .3.4 Beispiel
Ändern des zu verwendenden Funktionszeigerbeispiels
std::function
gibt dieser Funktion viel mehr Nutzen, weil wir (siehe 3.3) mehr Möglichkeiten haben, sie zu verwenden:
4. Templated Callback-Typ
Bei Verwendung von Vorlagen kann der Code, der den Rückruf aufruft, noch allgemeiner sein als bei Verwendung von
std::function
Objekten.Beachten Sie, dass Vorlagen eine Funktion zur Kompilierungszeit und ein Entwurfswerkzeug für den Polymorphismus zur Kompilierungszeit sind. Wenn das dynamische Verhalten zur Laufzeit durch Rückrufe erreicht werden soll, helfen Vorlagen, führen jedoch nicht zur Laufzeitdynamik.
4.1 Schreiben (Typnotationen) und Aufrufen von Rückrufen mit Vorlagen
Eine weitere Verallgemeinerung, dh des
std_ftransform_every_int
Codes von oben, kann mithilfe von Vorlagen erreicht werden:mit einer noch allgemeineren (sowie einfachsten) Syntax für einen Rückruftyp, die ein einfaches, abzuleitendes Argument mit Vorlagen ist:
Hinweis: Die enthaltene Ausgabe gibt den Typnamen aus, der für den Vorlagen-Typ abgeleitet wurde
F
. Die Implementierung vontype_name
wird am Ende dieses Beitrags angegeben.Die allgemeinste Implementierung für die unäre Transformation eines Bereichs ist Teil der Standardbibliothek
std::transform
, die auch in Bezug auf die iterierten Typen als Vorlage dient.4.2 Beispiele für Vorlagenrückrufe und kompatible Typen
Die kompatiblen Typen für die Templated
std::function
Callback-Methodestdf_transform_every_int_templ
sind identisch mit den oben genannten Typen (siehe 3.4).Bei Verwendung der Vorlagenversion kann sich die Signatur des verwendeten Rückrufs jedoch geringfügig ändern:
Hinweis:
std_ftransform_every_int
(Version ohne Vorlage; siehe oben) funktioniert mit, wirdfoo
aber nicht verwendetmuh
.Der einfache Vorlagenparameter von
transform_every_int_templ
kann jeder mögliche aufrufbare Typ sein.Der obige Code wird gedruckt:
type_name
Implementierung oben verwendetquelle
int b = foobar(a, foo); // call foobar with pointer to foo as callback
, das ist ein Tippfehler, oder?foo
sollte ein Zeiger dafür sein, damit AFAIK funktioniert.[conv.func]
des C ++ 11-Standards sagt: " Ein Wert vom Funktionstyp T kann in einen Wert vom Typ" Zeiger auf T "konvertiert werden." Das Ergebnis ist ein Zeiger auf die Funktion. "Dies ist eine Standardkonvertierung und erfolgt daher implizit. Man könnte hier (natürlich) den Funktionszeiger verwenden.Es gibt auch die C-Methode für Rückrufe: Funktionszeiger
Wenn Sie nun Klassenmethoden als Rückrufe übergeben möchten, haben die Deklarationen an diese Funktionszeiger komplexere Deklarationen. Beispiel:
quelle
typedef
den Rückruftyp zu verwenden? Ist es überhaupt möglich?typedef
ist nur syntaktischer Zucker, um es lesbarer zu machen. Ohne wäretypedef
die Definition von DoWorkObject für Funktionszeiger :void DoWorkObject(int (*callback)(float))
. Für Mitgliederzeiger wären:void DoWorkObject(int (ClassName::*callback)(float))
Scott Meyers gibt ein schönes Beispiel:
Ich denke, das Beispiel sagt alles.
std::function<>
ist die "moderne" Art, C ++ - Rückrufe zu schreiben.quelle
Eine Rückruffunktion ist eine Methode, die an eine Routine übergeben und irgendwann von der Routine aufgerufen wird, an die sie übergeben wird.
Dies ist sehr nützlich, um wiederverwendbare Software zu erstellen. Beispielsweise verwenden viele Betriebssystem-APIs (wie die Windows-API) häufig Rückrufe.
Wenn Sie beispielsweise mit Dateien in einem Ordner arbeiten möchten, können Sie eine API-Funktion mit Ihrer eigenen Routine aufrufen, und Ihre Routine wird einmal pro Datei im angegebenen Ordner ausgeführt. Dadurch kann die API sehr flexibel sein.
quelle
Die akzeptierte Antwort ist sehr nützlich und ziemlich umfassend. Das OP gibt jedoch an
Also los geht's, ab C ++ 11 sind
std::function
keine Funktionszeiger und ähnliches erforderlich:Dieses Beispiel ist übrigens irgendwie real, weil Sie die Funktion
print_hashes
mit verschiedenen Implementierungen von Hash-Funktionen aufrufen möchten. Zu diesem Zweck habe ich eine einfache bereitgestellt. Es empfängt eine Zeichenfolge, gibt ein int zurück (einen Hashwert der bereitgestellten Zeichenfolge), und alles,std::function<int (const std::string&)>
woran Sie sich aus dem Syntaxteil erinnern müssen, ist , eine solche Funktion als Eingabeargument der Funktion zu beschreiben, die sie aufruft.quelle
In C ++ gibt es kein explizites Konzept für eine Rückruffunktion. Rückrufmechanismen werden häufig über Funktionszeiger, Funktorobjekte oder Rückrufobjekte implementiert. Die Programmierer müssen die Rückruffunktion explizit entwerfen und implementieren.
Bearbeiten basierend auf Feedback:
Trotz des negativen Feedbacks, das diese Antwort erhalten hat, ist es nicht falsch. Ich werde versuchen, besser zu erklären, woher ich komme.
C und C ++ bieten alles, was Sie zum Implementieren von Rückruffunktionen benötigen. Die häufigste und trivialste Methode zum Implementieren einer Rückruffunktion besteht darin, einen Funktionszeiger als Funktionsargument zu übergeben.
Rückruffunktionen und Funktionszeiger sind jedoch nicht gleichbedeutend. Ein Funktionszeiger ist ein Sprachmechanismus, während eine Rückruffunktion ein semantisches Konzept ist. Funktionszeiger sind nicht die einzige Möglichkeit, eine Rückruffunktion zu implementieren. Sie können auch Funktoren und sogar virtuelle Funktionen für verschiedene Gartenarten verwenden. Was einen Funktionsaufruf zu einem Rückruf macht, ist nicht der Mechanismus zum Identifizieren und Aufrufen der Funktion, sondern der Kontext und die Semantik des Aufrufs. Zu sagen, dass etwas eine Rückruffunktion ist, impliziert eine überdurchschnittliche Trennung zwischen der aufrufenden Funktion und der aufgerufenen spezifischen Funktion, eine lockerere konzeptionelle Kopplung zwischen dem Anrufer und dem Angerufenen, wobei der Anrufer die explizite Kontrolle darüber hat, was aufgerufen wird.
In der .NET-Dokumentation für IFormatProvider heißt es beispielsweise, dass "GetFormat eine Rückrufmethode ist" , obwohl es sich nur um eine gewöhnliche Schnittstellenmethode handelt. Ich glaube nicht, dass irgendjemand argumentieren würde, dass alle virtuellen Methodenaufrufe Rückruffunktionen sind. Was GetFormat zu einer Rückrufmethode macht, ist nicht die Mechanik, wie es übergeben oder aufgerufen wird, sondern die Semantik des Aufrufers, der die GetFormat-Methode des Objekts auswählt.
Einige Sprachen enthalten Funktionen mit expliziter Rückrufsemantik, die sich normalerweise auf Ereignisse und die Ereignisbehandlung beziehen. Zum Beispiel hat C # den Ereignistyp mit Syntax und Semantik, die explizit auf das Konzept von Rückrufen ausgelegt sind. Visual Basic verfügt über die Handles- Klausel, die eine Methode explizit als Rückruffunktion deklariert und gleichzeitig das Konzept von Delegaten oder Funktionszeigern abstrahiert. In diesen Fällen ist das semantische Konzept eines Rückrufs in die Sprache selbst integriert.
C und C ++ hingegen binden das semantische Konzept der Rückruffunktionen nicht annähernd so explizit ein. Die Mechanismen sind da, die integrierte Semantik nicht. Sie können Rückruffunktionen problemlos implementieren, aber um etwas Anspruchsvolleres zu erhalten, das eine explizite Rückrufsemantik enthält, müssen Sie es auf dem aufbauen, was C ++ bietet, beispielsweise was Qt mit seinen Signalen und Slots gemacht hat .
Kurz gesagt, C ++ bietet alles, was Sie zum Implementieren von Rückrufen benötigen, häufig recht einfach und trivial mithilfe von Funktionszeigern. Was es nicht gibt, sind Schlüsselwörter und Funktionen, deren Semantik für Rückrufe spezifisch ist, wie z. B. Erhöhen , Ausgeben , Handles , Ereignis + = usw. Wenn Sie aus einer Sprache mit diesen Elementtypen stammen, wird die native Rückrufunterstützung in C ++ unterstützt wird sich kastriert fühlen.
quelle
Rückruffunktionen sind Teil des C-Standards und daher auch Teil von C ++. Wenn Sie jedoch mit C ++ arbeiten, würde ich empfehlen , stattdessen das Beobachtermuster zu verwenden: http://en.wikipedia.org/wiki/Observer_pattern
quelle
Siehe die obige Definition, in der angegeben wird, dass eine Rückruffunktion an eine andere Funktion übergeben und irgendwann aufgerufen wird.
In C ++ ist es wünschenswert, dass Rückruffunktionen eine Klassenmethode aufrufen. Wenn Sie dies tun, haben Sie Zugriff auf die Mitgliedsdaten. Wenn Sie die C-Methode zum Definieren eines Rückrufs verwenden, müssen Sie ihn auf eine statische Elementfunktion verweisen. Dies ist nicht sehr wünschenswert.
So können Sie Rückrufe in C ++ verwenden. Angenommen, 4 Dateien. Ein Paar .CPP / .H-Dateien für jede Klasse. Klasse C1 ist die Klasse mit einer Methode, die wir zurückrufen möchten. C2 ruft die Methode von C1 auf. In diesem Beispiel verwendet die Rückruffunktion 1 Parameter, den ich für die Leser hinzugefügt habe. Das Beispiel zeigt keine Objekte, die instanziiert und verwendet werden. Ein Anwendungsfall für diese Implementierung ist, wenn Sie eine Klasse haben, die Daten liest und im temporären Speicherplatz speichert, und eine andere, die die Daten nachbearbeitet. Mit einer Rückruffunktion kann der Rückruf diese für jede gelesene Datenzeile verarbeiten. Diese Technik reduziert den Overhead des erforderlichen temporären Platzes. Dies ist besonders nützlich für SQL-Abfragen, die eine große Datenmenge zurückgeben, die dann nachbearbeitet werden muss.
quelle
Mit den Signalen2 von Boost2 können Sie generische Mitgliedsfunktionen (ohne Vorlagen!) Und threadsicher abonnieren.
quelle
Die akzeptierte Antwort ist umfassend, bezieht sich aber auf die Frage, die ich hier nur als einfaches Beispiel anführen möchte. Ich hatte einen Code, den ich vor langer Zeit geschrieben hatte. Ich wollte einen Baum in der richtigen Reihenfolge durchlaufen (linker Knoten, dann Wurzelknoten, dann rechter Knoten) und wann immer ich zu einem Knoten komme, wollte ich in der Lage sein, eine beliebige Funktion aufzurufen, damit er alles kann.
quelle