Sollte ich beim Implementieren einer Rückruffunktion in C ++ weiterhin den Funktionszeiger im C-Stil verwenden:
void (*callbackFunc)(int);
Oder sollte ich std :: function verwenden:
std::function< void(int) > callbackFunc;
Sollte ich beim Implementieren einer Rückruffunktion in C ++ weiterhin den Funktionszeiger im C-Stil verwenden:
void (*callbackFunc)(int);
Oder sollte ich std :: function verwenden:
std::function< void(int) > callbackFunc;
Antworten:
Kurz gesagt, verwendenstd::function
Sie es , es sei denn, Sie haben einen Grund, dies nicht zu tun.
Funktionszeiger haben den Nachteil, dass sie keinen Kontext erfassen können . Sie können beispielsweise keine Lambda-Funktion als Rückruf übergeben, der einige Kontextvariablen erfasst (dies funktioniert jedoch, wenn keine erfasst werden). Das Aufrufen einer Mitgliedsvariablen eines Objekts (dh nicht statisch) ist daher ebenfalls nicht möglich, da das Objekt ( this
-zeiger) erfasst werden muss. (1)
std::function
(seit C ++ 11) dient hauptsächlich zum Speichern einer Funktion (für die Weitergabe muss sie nicht gespeichert werden). Wenn Sie den Rückruf beispielsweise in einer Mitgliedsvariablen speichern möchten, ist dies wahrscheinlich die beste Wahl. Aber auch wenn Sie es nicht speichern, ist es eine gute "erste Wahl", obwohl es den Nachteil hat, dass beim Aufrufen ein gewisser (sehr kleiner) Overhead entsteht (in einer sehr leistungskritischen Situation kann dies ein Problem sein, aber in den meisten Fällen) es sollte nicht). Es ist sehr "universell": Wenn Sie großen Wert auf konsistenten und lesbaren Code legen und nicht über jede von Ihnen getroffene Auswahl nachdenken möchten (dh sie einfach halten möchten), verwenden Sie sie std::function
für jede Funktion, die Sie weitergeben.
Denken Sie an eine dritte Option: Wenn Sie eine kleine Funktion implementieren möchten, die dann über die bereitgestellte Rückruffunktion etwas meldet, ziehen Sie einen Vorlagenparameter in Betracht , der dann ein beliebiges aufrufbares Objekt sein kann , dh ein Funktionszeiger, ein Funktor, ein Lambda, a std::function
, ... Nachteil hier ist, dass Ihre (äußere) Funktion zu einer Vorlage wird und daher im Header implementiert werden muss. Andererseits haben Sie den Vorteil, dass der Aufruf des Rückrufs eingebunden werden kann, da der Client-Code Ihrer (äußeren) Funktion den Aufruf des Rückrufs "sieht", dass die genauen Typinformationen verfügbar sind.
Beispiel für die Version mit dem Template-Parameter (schreiben &
statt &&
für Pre-C ++ 11):
template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
...
callback(...);
...
}
Wie Sie in der folgenden Tabelle sehen können, haben alle ihre Vor- und Nachteile:
+-------------------+--------------+---------------+----------------+
| | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture | no(1) | yes | yes |
| context variables | | | |
+-------------------+--------------+---------------+----------------+
| no call overhead | yes | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be inlined | no | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be stored | yes | yes | no(2) |
| in class member | | | |
+-------------------+--------------+---------------+----------------+
| can be implemented| yes | yes | no |
| outside of header | | | |
+-------------------+--------------+---------------+----------------+
| supported without | yes | no(3) | yes |
| C++11 standard | | | |
+-------------------+--------------+---------------+----------------+
| nicely readable | no | yes | (yes) |
| (my opinion) | (ugly type) | | |
+-------------------+--------------+---------------+----------------+
(1) Es gibt Problemumgehungen, um diese Einschränkung zu überwinden, z. B. die Übergabe der zusätzlichen Daten als weitere Parameter an Ihre (äußere) Funktion: myFunction(..., callback, data)
wird aufgerufen callback(data)
. Dies ist der C-artige "Rückruf mit Argumenten", der in C ++ möglich ist (und übrigens in der WIN32-API häufig verwendet wird), aber vermieden werden sollte, da wir in C ++ bessere Optionen haben.
(2) Sofern es sich nicht um eine Klassenvorlage handelt, dh die Klasse, in der Sie die Funktion speichern, ist eine Vorlage. Dies würde jedoch bedeuten, dass auf der Clientseite der Typ der Funktion den Typ des Objekts bestimmt, in dem der Rückruf gespeichert ist, was für tatsächliche Anwendungsfälle fast nie eine Option ist.
(3) Verwenden Sie für Pre-C ++ 11 boost::function
std::function
Typ gelöschte konvertiert werden kann, wenn Sie sie außerhalb der sofort speichern müssen genannt Kontext).
void (*callbackFunc)(int);
Möglicherweise handelt es sich um eine Rückruffunktion im C-Stil, aber es ist eine schrecklich unbrauchbare Funktion mit schlechtem Design.
Ein gut gestalteter Rückruf im C-Stil sieht so aus void (*callbackFunc)(void*, int);
: Er void*
ermöglicht es dem Code, der den Rückruf ausführt, den Status über die Funktion hinaus beizubehalten. Andernfalls wird der Aufrufer gezwungen, den Status global zu speichern, was unhöflich ist.
std::function< int(int) >
int(*)(void*, int)
Dies ist in den meisten Implementierungen etwas teurer als der Aufruf. Für einige Compiler ist es jedoch schwieriger, Inline zu erstellen. Es gibt std::function
Klonimplementierungen, die den Aufwand für das Aufrufen von Funktionszeigern (siehe "Schnellstmögliche Delegaten" usw.) messen und möglicherweise in Bibliotheken gelangen.
Heutzutage müssen Clients eines Rückrufsystems häufig Ressourcen einrichten und entsorgen, wenn der Rückruf erstellt und entfernt wird, und die Lebensdauer des Rückrufs kennen. void(*callback)(void*, int)
bietet dies nicht.
Manchmal ist dies über die Codestruktur (der Rückruf hat eine begrenzte Lebensdauer) oder über andere Mechanismen (Aufheben der Registrierung von Rückrufen und dergleichen) möglich.
std::function
bietet ein Mittel zur Verwaltung der begrenzten Lebensdauer (die letzte Kopie des Objekts verschwindet, wenn es vergessen wird).
Im Allgemeinen würde ich ein verwenden, es std::function
sei denn, Leistungsbedenken manifestieren sich. Wenn ja, würde ich zuerst nach strukturellen Änderungen suchen (anstelle eines Rückrufs pro Pixel, wie wäre es, einen Scanline-Prozessor basierend auf dem Lambda zu generieren, den Sie mir übergeben? Dies sollte ausreichen, um den Aufwand für Funktionsaufrufe auf triviale Ebenen zu reduzieren. ). Wenn es dann weiterhin besteht, würde ich einen delegate
basierend auf den schnellstmöglichen Delegierten schreiben und prüfen, ob das Leistungsproblem behoben ist.
Ich würde meistens nur Funktionszeiger für ältere APIs oder zum Erstellen von C-Schnittstellen für die Kommunikation zwischen verschiedenen vom Compiler generierten Codes verwenden. Ich habe sie auch als interne Implementierungsdetails verwendet, wenn ich Sprungtabellen implementiere, Löschvorgänge schreibe usw .: wenn ich sie sowohl produziere als auch konsumiere und sie nicht extern für die Verwendung durch Clientcode verfügbar mache und Funktionszeiger alles tun, was ich brauche .
Beachten Sie, dass Sie Wrapper schreiben können, die a std::function<int(int)>
in einen int(void*,int)
Stil-Rückruf verwandeln , vorausgesetzt, es gibt eine ordnungsgemäße Infrastruktur für die Verwaltung der Rückruflebensdauer. Als Rauchtest für jedes C-artige Callback-Lifetime-Management-System würde ich sicherstellen, dass das Verpacken eines Systems std::function
einigermaßen gut funktioniert.
void*
kommt das? Warum sollten Sie den Zustand über die Funktion hinaus beibehalten wollen? Eine Funktion sollte den gesamten benötigten Code und alle Funktionen enthalten. Übergeben Sie einfach die gewünschten Argumente und ändern Sie etwas und geben Sie etwas zurück. Wenn Sie einen externen Status benötigen, warum sollte dann ein functionPtr oder ein Rückruf dieses Gepäck tragen? Ich denke, dass Rückruf unnötig komplex ist.
this
. Liegt es daran, dass der Fall eines Aufrufs einer Mitgliedsfunktion berücksichtigt werden muss, sodass der this
Zeiger auf die Adresse des Objekts zeigen muss? Wenn ich falsch liege, können Sie mir einen Link geben, über den ich weitere Informationen dazu finden kann, da ich nicht viel darüber finden kann. Danke im Voraus.
void*
um die Übertragung des Laufzeitstatus zu ermöglichen. Ein Funktionszeiger mit einem void*
und einem void*
Argument kann einen Elementfunktionsaufruf für ein Objekt emulieren. Entschuldigung, ich kenne keine Ressource, die "Entwerfen von C-Rückrufmechanismen 101" durchläuft.
this
. Das ist es was ich meinte. OK, trotzdem danke.
Verwenden Sie std::function
diese Option, um beliebige aufrufbare Objekte zu speichern. Der Benutzer kann den für den Rückruf erforderlichen Kontext angeben. Ein einfacher Funktionszeiger funktioniert nicht.
Wenn Sie aus irgendeinem Grund einfache Funktionszeiger verwenden müssen (möglicherweise, weil Sie eine C-kompatible API wünschen), sollten Sie ein void * user_context
Argument hinzufügen , damit es zumindest möglich (wenn auch unpraktisch) ist, auf den Status zuzugreifen, der nicht direkt an den übergeben wird Funktion.
Der einzige Grund, den Sie vermeiden sollten, std::function
ist die Unterstützung älterer Compiler, die diese in C ++ 11 eingeführte Vorlage nicht unterstützen.
Wenn die Unterstützung der Sprache vor C ++ 11 nicht erforderlich ist, haben std::function
Ihre Anrufer bei der Implementierung des Rückrufs mehr Auswahlmöglichkeiten, was ihn zu einer besseren Option im Vergleich zu "einfachen" Funktionszeigern macht. Es bietet den Benutzern Ihrer API mehr Auswahlmöglichkeiten und abstrahiert gleichzeitig die Besonderheiten ihrer Implementierung für Ihren Code, der den Rückruf ausführt.