it-swarm.com.de

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

Sollte ich bei der Implementierung einer Rückruffunktion in C++ den Funktionszeiger im C-Stil verwenden:

void (*callbackFunc)(int);

Oder sollte ich die Funktion std :: verwenden:

std::function< void(int) > callbackFunc;
118
worker11811

Verwenden Sie std::function, es sei denn, Sie haben einen Grund, dies nicht zu tun.

Funktionszeiger haben den Nachteil, dass nicht in der Lage ist einige Kontexte zu erfassen. Sie können beispielsweise eine Lambda-Funktion nicht als Callback übergeben, die einige Kontextvariablen erfasst (aber sie funktioniert, wenn sie keine erfasst). Das Aufrufen einer Member-Variablen eines Objekts (d. H. Nicht statisch) ist daher auch nicht möglich, da das Objekt (this- Pointer) erfasst werden muss.(1)

std::function (seit C++ 11) ist in erster Linie store einer Funktion (die Weitergabe dieser Funktion erfordert keine Speicherung). Wenn Sie den Rückruf beispielsweise in einer Member-Variablen 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, einen (sehr geringen) Overhead zu verursachen, wenn es aufgerufen wird (in einer sehr leistungskritischen Situation kann es jedoch ein Problem sein, aber in den meisten Fällen) es sollte nicht). Es ist sehr "universell": Wenn Sie viel Wert auf konsistenten und lesbaren Code legen und nicht über jede von Ihnen getroffene Auswahl nachdenken möchten (d. H., Sie wollen es einfach halten), verwenden Sie std::function für jede Funktion, die Sie weitergeben.

Denken Sie an eine dritte Option: Wenn Sie eine kleine Funktion implementieren, die dann über die bereitgestellte Callback-Funktion etwas meldet, sollten Sie ein template parameter in Betracht ziehen, das dann jedes aufrufbare Objekt sein kann Ein Funktionszeiger, ein Funktionscode, ein Lambda, ein std::function, ... Nachteil ist hier, dass Ihre (äußere) Funktion eine Vorlage wird und daher im Header implementiert werden muss. Auf der anderen Seite haben Sie den Vorteil, dass der Aufruf des Callbacks eingebettet werden kann, da der Clientcode Ihrer (äußeren) Funktion den Aufruf des Callbacks "sieht" und die genauen Typinformationen verfügbar sind.

Beispiel für die Version mit dem Template-Parameter (schreiben Sie & anstelle von && für Pre-C++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Wie Sie der folgenden Tabelle entnehmen 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 Abhilfemaßnahmen, um diese Einschränkung zu überwinden. Beispielsweise werden die zusätzlichen Daten als weitere Parameter an Ihre (äußere) Funktion übergeben: myFunction(..., callback, data) ruft callback(data) auf. Das ist der "Callback mit Argumenten" im C-Stil, der in C++ möglich ist (und übrigens in der WIN32-API stark verwendet wird), sollte aber vermieden werden, da wir in C++ bessere Optionen haben.

(2) Es sei denn, es handelt sich um eine Klassenvorlage, d. H. 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 festlegt, in dem der Rückruf gespeichert wird, was für tatsächliche Anwendungsfälle fast nie möglich ist.

(3) Verwenden Sie für Pre-C++ 11 boost::function

148
leemes

void (*callbackFunc)(int); kann eine Rückruffunktion im C-Stil sein, aber sie ist schrecklich unbrauchbar und hat ein schlechtes Design.

Ein gut entworfener C-Style-Callback sieht aus wie void (*callbackFunc)(void*, int); - er hat ein void*, damit der Code, der den Callback ausführt, den Zustand außerhalb der Funktion beibehalten kann. Andernfalls muss der Anrufer den Status global speichern, was unhöflich ist.

std::function< int(int) > ist in den meisten Implementierungen etwas teurer als int(*)(void*, int). Für einige Compiler ist es jedoch schwieriger, Inline zu integrieren. Es gibt std::function-Klonimplementierungen, die mit dem Aufruf von Overheads für Funktionszeiger konkurrieren können (siehe 'schnellstmögliche Delegaten' usw.), die möglicherweise in Bibliotheken Einzug halten.

Nun müssen Clients eines Rückrufsystems häufig Ressourcen einrichten und entsorgen, wenn der Rückruf erstellt und entfernt wird, und sich der Lebensdauer des Rückrufs bewusst sein. void(*callback)(void*, int) bietet dies nicht an.

Manchmal ist dies über die Codestruktur (der Callback hat eine begrenzte Lebensdauer) oder über andere Mechanismen (Aufheben der Registrierung von Callbacks und dergleichen) verfügbar.

std::function bietet eine Möglichkeit zur eingeschränkten Lebenszeitverwaltung (die letzte Kopie des Objekts wird gelöscht, wenn es vergessen wird).

Im Allgemeinen würde ich ein std::function verwenden, es sei denn, die Leistung betrifft ein Manifest. Wenn dies der Fall wäre, 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, das Sie mir übergeben, zu generieren?). ). Wenn dies fortbesteht, würde ich eine delegate-basierte schnellstmögliche Delegierte schreiben und sehen, ob das Leistungsproblem beseitigt ist.

Ich würde meistens Funktionszeiger nur für ältere APIs oder für die Erstellung von C-Schnittstellen für die Kommunikation zwischen dem von verschiedenen Compilern generierten Code verwenden. Ich habe sie auch als interne Implementierungsdetails verwendet, wenn ich Sprungtabellen implementiere, Typen lösche usw. usw., wenn ich sie sowohl produziere als auch verbrauche und sie nicht extern für jeden Clientcode verfügbar mache und Funktionszeiger alles, was ich brauche .

Beachten Sie, dass Sie Wrapper schreiben können, die eine std::function<int(int)> in einen Callback im int(void*,int)-Stil verwandeln, vorausgesetzt, es gibt eine angemessene Infrastruktur für die Verwaltung der Callback-Lebensdauer. Als Rauchtest für jedes C-Callback-Lifetime-Managementsystem würde ich sicherstellen, dass das Wrapping eines std::function ziemlich gut funktioniert.

Verwenden Sie std::function, um beliebige aufrufbare Objekte zu speichern. Damit kann der Benutzer den Kontext angeben, der für den Rückruf erforderlich ist. ein einfacher Funktionszeiger funktioniert nicht.

Wenn Sie aus irgendeinem Grund einfache Funktionszeiger verwenden müssen (vielleicht, weil Sie eine C-kompatible API möchten), sollten Sie ein void * user_context-Argument hinzufügen, damit es zumindest (wenn auch unbequem) auf den Status zugreifen kann, auf den nicht direkt weitergeleitet wird die Funktion.

16
Mike Seymour

Der einzige Grund, std::function zu vermeiden, ist die Unterstützung von älteren Compilern, die diese in C++ 11 eingeführte Vorlage nicht unterstützen.

Wenn die Unterstützung der Sprache vor C++ 11 nicht erforderlich ist, können Anrufer mithilfe von std::function mehr Wahlmöglichkeiten beim Implementieren des Rückrufs haben. Dies macht sie im Vergleich zu "einfachen" Funktionszeigern zu einer besseren Option. Es bietet den Benutzern Ihrer API mehr Auswahlmöglichkeiten und abstrahiert die Details ihrer Implementierung für Ihren Code, der den Rückruf ausführt.

13
dasblinkenlight

std::function kann in einigen Fällen VMT zum Code bringen, was sich auf die Leistung auswirkt.

0
vladon