Soll ich in C ++ std :: function oder einen Funktionszeiger verwenden?


141

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;

9
Wenn die Rückruffunktion zur Kompilierungszeit bekannt ist, ziehen Sie stattdessen eine Vorlage in Betracht.
Baum mit Augen

4
Bei der Implementierung einer Rückruffunktion sollten Sie alles tun, was der Anrufer benötigt. Wenn es bei Ihrer Frage wirklich um das Entwerfen einer Rückrufschnittstelle geht, gibt es hier bei weitem nicht genügend Informationen, um sie zu beantworten. Was soll der Empfänger Ihres Rückrufs tun? Welche Informationen müssen Sie an den Empfänger weitergeben? Welche Informationen sollte der Empfänger als Ergebnis des Anrufs an Sie zurückgeben?
Pete Becker

Antworten:


171

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::functionfü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


9
Funktionszeiger haben im Vergleich zu Vorlagenparametern einen Aufrufaufwand. Vorlagenparameter erleichtern das Inlining, selbst wenn Sie über vierzehn Ebenen übergeben werden, da der ausgeführte Code durch den Typ des Parameters und nicht durch den Wert beschrieben wird. Und Vorlagenfunktionsobjekte, die in Vorlagenrückgabetypen gespeichert werden, sind ein allgemeines und nützliches Muster (mit einem guten Kopierkonstruktor können Sie die aufrufbare effiziente Vorlagenfunktion erstellen, die in die vom std::functionTyp gelöschte konvertiert werden kann, wenn Sie sie außerhalb der sofort speichern müssen genannt Kontext).
Yakk - Adam Nevraumont

1
@tohecz Ich erwähne jetzt, ob es C ++ 11 erfordert oder nicht.
Leemes

1
@ Yakk Oh natürlich, das habe ich vergessen! Fügte es hinzu, danke.
Leemes

1
@MooingDuck Natürlich kommt es auf die Implementierung an. Aber wenn ich mich richtig erinnere, findet aufgrund der Funktionsweise des Löschens eine weitere Indirektion statt? Aber jetzt, wo ich noch einmal darüber nachdenke, denke ich, dass dies nicht der Fall ist, wenn Sie ihm Funktionszeiger oder
lambdas ohne

1
@leemes: Richtig, für Funktionszeiger oder Captureless Lambdas sollte es den gleichen Overhead haben wie ein c-func-ptr. Welches ist immer noch ein Pipeline-Stall + nicht trivial inline.
Mooing Duck

24

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::functionKlonimplementierungen, 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::functionsei 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 delegatebasierend 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::functioneinigermaßen gut funktioniert.


1
Woher 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.
Nikos

@ nik-lz Ich bin mir nicht sicher, wie ich Ihnen in einem Kommentar die Verwendung und den Verlauf von Rückrufen in C beibringen würde. Oder die Philosophie des Verfahrens im Gegensatz zur funktionalen Programmierung. Sie werden also unerfüllt bleiben.
Yakk - Adam Nevraumont

Ich habe vergessen this. Liegt es daran, dass der Fall eines Aufrufs einer Mitgliedsfunktion berücksichtigt werden muss, sodass der thisZeiger 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.
Nikos

@ Nik-Lz Mitgliedsfunktionen sind keine Funktionen. Funktionen haben keinen (Laufzeit-) Status. Rückrufe werden benötigt, 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.
Yakk - Adam Nevraumont

Ja, darüber habe ich gesprochen. Der Laufzeitstatus ist im Grunde die Adresse des aufgerufenen Objekts (da es sich zwischen den Läufen ändert). Es geht immer noch darum this. Das ist es was ich meinte. OK, trotzdem danke.
Nikos

17

Verwenden Sie std::functiondiese 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_contextArgument hinzufügen , damit es zumindest möglich (wenn auch unpraktisch) ist, auf den Status zuzugreifen, der nicht direkt an den übergeben wird Funktion.


Was ist die Art von p hier? Wird es ein std :: Funktionstyp sein? void f () {}; auto p = f; p ();
Sree

14

Der einzige Grund, den Sie vermeiden sollten, std::functionist 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::functionIhre 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.


1

std::function In einigen Fällen kann VMT zum Code gebracht werden, was sich auf die Leistung auswirkt.


3
Können Sie erklären, was diese VMT ist?
Gupta
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.