Standard C++
Zeiger auf Member-Funktionen
Unterscheidet sich der Typ von „Pointer-to-member-function“ von „Pointer-to-function“?
Ja.
Betrachte die folgende Funktion:
int f(char a, float b);
Der Typ dieser Funktion ist unterschiedlich, je nachdem, ob es sich um eine gewöhnliche Funktion oder eine Nicht-static
-Mitgliedsfunktion einer Klasse handelt:
Anmerkung: Wenn es sich um eine static
-Mitgliedsfunktion von class
Fred
handelt, ist ihr Typ derselbe wie bei einer gewöhnlichen Funktion:“int (*)(char,float)
„.
Wie übergebe ich einen Zeiger auf eine Mitgliedsfunktion an einen Signalhandler, einen X-Ereignis-Callback, einen Systemaufruf, der einen Thread/Task startet, usw.?
Nicht.
Da eine Member-Funktion ohne ein Objekt, mit dem sie aufgerufen werden kann, bedeutungslos ist, kann man dies nicht direkt tun (wenn das X WindowSystem in C++ neu geschrieben würde, würde es wahrscheinlich Referenzen auf Objekte weitergeben, nicht nur Zeiger auf Funktionen; natürlich würden die Objekte die erforderliche Funktion und wahrscheinlich eine ganze Menge mehr verkörpern).
Als Patch für bestehende Software kann man eine Top-Level-Funktion (die nicht Mitglied ist) als Wrapper verwenden, die ein Objekt aufnimmt, das durch eine andere Technik erhalten wurde. Je nach der Routine, die Sie aufrufen, kann diese „andere Technik“ trivial sein oder ein wenig Arbeit Ihrerseits erfordern. Der Systemaufruf, der einen Thread startet, könnte beispielsweise erfordern, dass Sie einen Funktionszeiger zusammen mit einem void*
übergeben, so dass Sie den Objektzeiger im void*
übergeben können. Viele Echtzeit-Betriebssysteme machen etwas Ähnliches für die Funktion, die eine neue Aufgabe startet. Im schlimmsten Fall könnten Sie den Objektzeiger in einer globalen Variablen speichern; dies könnte für Unix-Signalhandler erforderlich sein (aber globale Variablen sind im Allgemeinen unerwünscht). In jedem Fall würde die Top-Level-Funktion die gewünschte Member-Funktion des Objekts aufrufen.
Hier ein Beispiel für den schlimmsten Fall (Verwendung einer globalen Variable). Nehmen wir an, Sie wollen Fred::memberFn()
bei einer Unterbrechung aufrufen:
Anmerkung: static
-Mitgliedsfunktionen benötigen kein tatsächliches Objekt, um aufgerufen zu werden, daher sind Zeiger-auf-static
-Mitgliedsfunktionen normalerweise typkompatibel mit regulären Zeigern auf Funktionen. Aber obwohl es wahrscheinlich auf den meisten Compilern funktioniert, müsste es eigentlich eine extern "C"
-Nicht-Member-Funktion sein, um korrekt zu sein, da „C-Linkage“ nicht nur Dinge wie Namensmangling abdeckt, sondern auch Aufrufkonventionen, die zwischen C und C++ unterschiedlich sein können.
Warum erhalte ich immer wieder Kompilierfehler (type mismatch), wenn ich versuche, eine Member-Funktion als Interrupt-Service-Routine zu verwenden?
Dies ist ein Spezialfall der beiden vorhergehenden Fragen, lesen Sie daher zuerst die beiden vorhergehenden Antworten.
Nicht-static
Member-Funktionen haben einen versteckten Parameter, der dem this
Zeiger entspricht. Der this
-Zeiger zeigt auf die Instanzdaten des Objekts. Die Interrupt-Hardware/Firmware im System ist nicht in der Lage, das Argument des this
-Zeigers zu liefern. Sie müssen „normale“ Funktionen (Nicht-Klassenmitglieder) oder static
-Mitgliedsfunktionen als Unterbrechungsdienstroutinen verwenden.
Eine mögliche Lösung besteht darin, ein static
-Mitglied als Unterbrechungsdienstroutine zu verwenden und diese Funktion irgendwo nach dem Instanz/Mitgliedspaar suchen zu lassen, das bei einer Unterbrechung aufgerufen werden soll. Der Effekt ist also, dass eine Mitgliedsfunktion bei einer Unterbrechung aufgerufen wird, aber aus technischen Gründen müssen Sie zuerst eine Zwischenfunktion aufrufen.
Warum habe ich Probleme, die Adresse einer C++-Funktion zu übernehmen?
Kurze Antwort: Wenn Sie versuchen, sie in einem Zeiger auf eine Funktion zu speichern (oder sie als Zeiger auf eine Funktion zu übergeben), dann ist das das Problem – dies ist eine Folge der vorherigen FAQ.
Lange Antwort: In C++ haben Mitgliedsfunktionen einen impliziten Parameter, der auf das Objekt zeigt (der this
Zeiger innerhalb der Mitgliedsfunktion). Normale C-Funktionen haben eine andere Aufrufkonvention als Member-Funktionen, so dass die Typen ihrer Zeiger (Zeiger-auf-Member-Funktion vs. Zeiger-auf-Funktion) unterschiedlich und nicht kompatibel sind. C++ führt einen neuen Zeigertyp ein, der Pointer-to-member genannt wird und nur durch die Übergabe eines Objekts aufgerufen werden kann.
HINWEIS: Versuchen Sie nicht, eine Pointer-to-member-Funktion in eine Pointer-to-function zu „casten“; das Ergebnis ist undefiniert und wahrscheinlich katastrophal. So ist es z.B. nicht erforderlich, dass ein Zeiger-auf-Glied-Funktion die Maschinenadresse der entsprechenden Funktion enthält. Wie im letzten Beispiel gesagt wurde, wenn Sie einen Zeiger auf eine reguläre C-Funktion haben, verwenden Sie entweder eine Funktion auf atop-Ebene (Nicht-Mitglied) oder eine static
(Klassen-) Mitglied-Funktion.
Wie kann ich Syntaxfehler vermeiden, wenn ich Zeiger auf Mitglieder erzeuge?
Verwenden Sie eine typedef
.
Ja, richtig, ich weiß: Sie sind anders. Du bist schlau. Du kannst diese Sachen ohne typedef
machen. Seufz. Ich habe viele E-Mails von Leuten erhalten, die sich wie Sie geweigert haben, die einfachen Ratschläge dieser FAQ zu befolgen. Sie haben Stunden und Stunden ihrer Zeit verschwendet, obwohl 10 Sekunden typedef
ihr Leben vereinfacht hätten. Außerdem, seien Sie ehrlich, Sie schreiben keinen Code, den nur Sie lesen können; Sie schreiben Ihren Code hoffentlich so, dass andere ihn auch lesen können – wenn sie müde sind – wenn sie ihre eigenen Fristen und Herausforderungen haben. Warum also sollten Sie sich selbst und anderen das Leben absichtlich schwer machen? Seien Sie klug: Verwenden Sie ein typedef
.
Hier ist eine Beispielklasse:
class Fred {public: int f(char x, float y); int g(char x, float y); int h(char x, float y); int i(char x, float y); // ...};
Die Typendefinition ist trivial:
typedef int (Fred::*FredMemFn)(char x, float y); // Please do this!
Das war’s! FredMemFn
ist der Typname, und ein Zeiger dieses Typs zeigt auf ein beliebiges Mitglied von Fred
, das(char,float)
annimmt, wie Fred
s f
, g
, h
und i
.
Es ist dann trivial, einen Member-Funktions-Zeiger zu deklarieren:
int main(){ FredMemFn p = &Fred::f; // ...}
Und es ist auch trivial, Funktionen zu deklarieren, die Member-Funktions-Zeiger empfangen:
void userCode(FredMemFn p){ /*...*/ }
Und es ist auch trivial, Funktionen zu deklarieren, die Member-Funktions-Zeiger zurückgeben:
FredMemFn userCode(){ /*...*/ }
So bitte, benutze ein typedef
. Entweder das oder schicken Sie mir keine E-Mail über die Probleme, die Sie mit Ihren Member-Funktionszeigern haben!
Wie kann ich Syntaxfehler vermeiden, wenn ich eine Member-Funktion mit einem Pointer-to-Member-Function aufrufe?
Wenn Sie Zugang zu einem Compiler und einer Standardbibliothek haben, die die entsprechenden Teile des kommenden C++17-Standards implementieren, verwenden Sie std::invoke
. Andernfalls verwenden Sie ein #define
-Makro.
Bitte.
Bitte sehr.
Ich bekomme viel zu viele E-Mails von verwirrten Leuten, die sich weigern, diesen Rat zu befolgen. Dabei ist es so einfach. Ich weiß, Sie brauchen wederstd::invoke
noch ein Makro, und der Experte, mit dem Sie gesprochen haben, kann es auch ohne beides machen, aber bitte lassen Sie Ihr Ego nicht dem im Wege stehen, was wichtig ist: Geld. Andere Programmierer werden Ihren Code lesen und pflegen müssen. Ja, ich weiß: Sie sind schlauer als alle anderen; gut. Und Sie sind großartig; gut. Aber füge deinem Code keine unnötige Komplexität hinzu.
Die Verwendung von std::invoke
ist trivial. Hinweis: FredMemFn
ist ein typedef
für einen Pointer-to-membertype:
Wenn du std::invoke
nicht verwenden kannst, reduziere die Wartungskosten, indem du in diesem speziellen Fall paradoxerweise ein #define
Makro verwendest.
(Normalerweise mag ich #define
-Makros nicht, aber man sollte sie mit Zeigern verwenden, weil sie die Lesbarkeit und Schreibbarkeit dieser Art von Code verbessern.)
Das Makro ist trivial:
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
Die Verwendung des Makros ist ebenfalls trivial. Anmerkung: FredMemFn
ist ein typedef
für einen Zeiger-auf-Membertype:
Der Grund, warum std::invoke
oder dieses Makro eine gute Idee ist, ist, dass Aufrufe von Memberfunktionen oft viel komplexer sind als das einfache Beispiel, das gerade gegeben wurde. Der Unterschied in der Lesbarkeit und Schreibbarkeit ist signifikant.comp.lang.c++
musste hunderte von Postings von verwirrten Programmierern ertragen, die die Syntax nicht richtig verstanden haben. Fast alle diese Fehler wären verschwunden, wenn sie std::invoke
oder das obige Makro benutzt hätten.
Anmerkung: #define
Makros sind auf 4 verschiedene Arten böse: böse#1, böse#2, böse#3 und böse#4. Aber sie sind trotzdem manchmal nützlich. Aber du solltest trotzdem ein vages Gefühl der Scham empfinden, nachdem du sie benutzt hast.
Wie erstelle und benutze ich ein Array von Pointer-to-member-function?
Benutze sowohl typedef
als auch std::invoke
oder das zuvor beschriebene #define
Makro, und du bist zu 90% fertig.
Schritt 1: Erstelle ein typedef
:
Schritt 2: Erstelle ein #define
-Makro, wenn du kein std::invoke
hast:
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
Jetzt ist dein Array von Zeigern auf Mitgliederfunktionen ganz einfach:
FredMemFn a = { &Fred::f, &Fred::g, &Fred::h, &Fred::i };
Und Ihre Verwendung eines Zeigers auf eine Mitgliedsfunktion ist ebenfalls einfach:
void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: std::invoke(a, fred, 'x', 3.14);}
oder wenn Sie std::invoke
nicht haben,
void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: CALL_MEMBER_FN(fred, a) ('x', 3.14);}
Anmerkung: #define
-Makros sind auf 4 verschiedene Arten böse: böse#1, böse#2, böse#3 und böse#4. Aber manchmal sind sie trotzdem nützlich. Schäme dich, fühle dich schuldig, aber wenn ein böses Konstrukt wie ein Makro deine Software verbessert, benutze es.
Wie deklariere ich einen Zeiger auf eine Member-Funktion, der auf eine Const Member-Funktion zeigt?
Kurze Antwort: Füge ein const
rechts vom )
hinzu, wenn du ein typedef
benutzt, um den Member-Funktions-Zeiger-Typ zu deklarieren.
Angenommen, du willst einen Zeiger-auf-Member-Funktion, der auf Fred::f
, Fred::g
oder Fred::h
zeigt:
class Fred {public: int f(int i) const; int g(int i) const; int h(int j) const; // ...};
Wenn du dann ein typedef
verwendest, um den Member-Function-Pointer-Typ zu deklarieren, sollte es wie folgt aussehen:
Das ist es!
Dann können Sie Member-Funktions-Zeiger deklarieren/übergeben/zurückgeben, so wie Sie es gewohnt sind:
Was ist der Unterschied zwischen den Operatoren .* und ->*?
Das brauchen Sie nicht zu verstehen, wenn Sie std::invoke
oder ein Makro für Member-Funktions-Zeiger-Aufrufe verwenden. Ohyea, bitte verwenden Sie std::invoke
oder ein Makro in diesem Fall. Und habe ich schon erwähnt, dass Sie in diesem Fall std::invoke
oder ein Makro verwenden sollten?!?
Zum Beispiel:
Aber ziehen Sie bitte in Erwägung, stattdessen ein std::invoke
oder ein Makro zu verwenden:
void sample(Fred x, Fred& y, Fred* z, FredMemFn func){ std::invoke(func, x, 42, 3.14); std::invoke(func, y, 42, 3.14); std::invoke(func, *z, 42, 3.14);}
oder
Wie bereits besprochen, sind Aufrufe in der realen Welt oft viel komplizierter als die einfachen hier, so dass die Verwendung eines std::invoke
oder eines Makros in der Regel die Schreib- und Lesbarkeit Ihres Codes verbessern wird.
Kann ich einen Zeiger auf eine Mitgliedsfunktion in ein void* umwandeln?
Nein!
Technische Details: Zeiger auf Mitgliedsfunktionen und Zeiger auf Daten werden nicht unbedingt auf die gleiche Weise dargestellt. Ein Zeiger auf eine Mitgliedsfunktion kann eher eine Datenstruktur sein als ein einzelner Zeiger. Denken Sie darüber nach: Wenn er auf eine virtuelle Funktion zeigt, zeigt er vielleicht nicht wirklich auf einen statisch auflösbaren Haufen Code, also könnte es nicht einmal eine normale Adresse sein – es könnte eine andere Datenstruktur irgendeiner Art sein.
Bitte schicken Sie mir keine E-Mail, wenn das oben genannte auf Ihrer speziellen Version Ihres speziellen Compilers auf Ihrem speziellen Betriebssystem zu funktionieren scheint. I don’t care. Es ist illegal, Punkt.
Kann ich einen Zeiger auf eine Funktion in ein void* umwandeln?
Nein!
Technische Details: void*
Zeiger sind Zeiger auf Daten, und Funktionszeiger zeigen auf Funktionen. Die Sprache verlangt nicht, dass Funktionen und Daten im gleichen Adressraum liegen, so dass, als Beispiel und nicht als Einschränkung, auf Architekturen, die sie in verschiedenen Adressräumen haben, die beiden verschiedenen Zeigertypen nicht vergleichbar sind.
Bitte schicken Sie mir keine E-Mail, wenn das oben genannte auf Ihrer speziellen Version Ihres speziellen Compilers auf Ihrem speziellen Betriebssystem zu funktionieren scheint. I don’t care. Es ist illegal, Punkt.
Ich brauche etwas wie Funktionszeiger, aber mit mehr Flexibilität und/oder Thread-Sicherheit; gibt es einen anderen Weg?
Verwenden Sie ein Functionoid.
Was zum Teufel ist ein Functionoid, und warum sollte ich eines verwenden?
Functionoids sind Funktionen auf Steroiden. Functionoids sind wesentlich leistungsfähiger als Funktionen, und diese zusätzliche Leistung löst einige (nicht alle) der Probleme, die bei der Verwendung von Funktionszeigern typischerweise auftreten.
Lassen Sie uns ein Beispiel für die traditionelle Verwendung von Funktionszeigern erarbeiten, und dann werden wir dieses Beispiel auf Functionoids übertragen. Die traditionelle Idee von Funktionszeigern ist es, eine Reihe von kompatiblen Funktionen zu haben:
int funct1( /*...params...*/ ) { /*...code...*/ }int funct2( /*...params...*/ ) { /*...code...*/ }int funct3( /*...params...*/ ) { /*...code...*/ }
Dann greift man auf diese über Funktionszeiger zu:
typedef int(*FunctPtr)( /*...params...*/ );void myCode(FunctPtr f){ // ... f( /*...args-go-here...*/ ); // ...}
Manchmal erstellt man ein Array dieser Funktionszeiger:
FunctPtr array;array = funct1;array = funct1;array = funct3;array = funct2;// ...
In diesem Fall ruft man die Funktion durch Zugriff auf das Array auf:
array( /*...args-go-here...*/ );
Bei Functionoids erstellt man zunächst eine Basisklasse mit einer rein virtuellen Methode:
Dann erstellt man statt drei Funktionen drei abgeleitete Klassen:
Dann übergibt man statt eines Funktionszeigers einen Funct*
. Ich werde ein typedef
mit dem Namen FunctPtr
erstellen, nur damit der Rest des Codes dem altmodischen Ansatz ähnelt:
typedef Funct* FunctPtr;void myCode(FunctPtr f){ // ... f->doit( /*...args-go-here...*/ ); // ...}
Sie können ein Array davon auf fast dieselbe Weise erstellen:
Das gibt uns den ersten Hinweis darauf, wo functionoids wirklich leistungsfähiger sind als function-pointers: die Tatsache, dass der functionoid-Ansatz Argumente hat, die Sie an die ctors übergeben können (oben als …ctor-args…), während die function-pointers-Version dies nicht tut. Stellen Sie sich ein functionoid-Objekt als einen gefriergetrockneten Funktionsaufruf vor (Betonung auf dem Wort „Aufruf“). Anders als ein Zeiger auf eine Funktion ist ein functionoid (konzeptionell) ein Zeiger auf eine teilweise aufgerufene Funktion. Stellen Sie sich eine Technologie vor, die es Ihnen ermöglicht, einige, aber nicht alle Argumente an eine Funktion zu übergeben und dann diesen (teilweise abgeschlossenen) Aufruf einzufrieren. Stellen Sie sich vor, diese Technologie gäbe Ihnen eine Art magischen Zeiger auf den gefriergetrockneten, teilweise abgeschlossenen Funktionsaufruf zurück. Später übergeben Sie dann die verbleibenden Argumente unter Verwendung dieses Zeigers, und das System nimmt auf magische Weise Ihre ursprünglichen Argumente (die gefriergetrocknet wurden), kombiniert sie mit allen lokalen Variablen, die die Funktion vor dem Einfrieren berechnet hat, kombiniert all das mit den neu übergebenen Argumenten und setzt die Ausführung der Funktion dort fort, wo sie beim Einfrieren aufgehört hat. Das mag wie Science Fiction klingen, ist aber genau das, was man mit Functionoids machen kann. Außerdem können Sie diese gefriergetrocknete Funktion mit verschiedenen „verbleibenden Parametern“ so oft wie Sie wollen „abschließen“. Außerdem erlauben (erfordern) sie es, den gefriergetrockneten Zustand zu ändern, wenn die Funktion aufgerufen wird, was bedeutet, dass Funktionoide sich Informationen von einem Aufruf zum nächsten merken können.
Lassen Sie uns die Füße wieder auf den Boden bekommen, und wir werden mit ein paar Beispielen arbeiten, um zu erklären, was all dieser Hokuspokus wirklich bedeutet.
Angenommen, die ursprünglichen Funktionen (im altmodischen Funktions-Zeiger-Stil) nahmen etwas andere Parameter.
int funct1(int x, float y){ /*...code...*/ }int funct2(int x, const std::string& y, int z){ /*...code...*/ }int funct3(int x, const std::vector<double>& y){ /*...code...*/ }
Wenn die Parameter unterschiedlich sind, ist der altmodische Funktionszeiger-Ansatz schwierig zu verwenden, da der Aufrufer nicht weiß, welche Parameter er übergeben soll (der Aufrufer hat nur einen Zeiger auf die Funktion, nicht den Namen der Funktion oder, wenn die Parameter unterschiedlich sind, die Anzahl und die Typen ihrer Parameter) (schreiben Sie mir keine E-Mail darüber; ja, Sie können es tun, aber Sie müssen auf dem Kopf stehen und unordentliche Dinge tun; aber schreiben Sie mir nicht darüber – verwenden Sie stattdessenFunktionsoide).
Mit Funktionoiden ist die Situation, zumindest manchmal, viel besser. Da man sich ein Functionoid als einen gefriergetrockneten Funktionsaufruf vorstellen kann, nimmt man einfach die nicht-üblichen Args, wie die, die ich y
und/oder z
genannt habe, und macht sie zu Args für die entsprechenden Ctors. Sie können auch die allgemeinen Args (in diesem Fall int
mit dem Namen x
) an den ctor übergeben, müssen es aber nicht – Sie haben die Möglichkeit, sie stattdessen an die rein virtuelle Methode doit()
zu übergeben. Ich nehme an, Sie wollen x
an doit()
und y
und/oder z
an die ctors übergeben:
class Funct {public: virtual int doit(int x) = 0;};
Dann erstellen Sie anstelle von drei Funktionen drei abgeleitete Klassen:
Jetzt sehen Sie, dass die Parameter des ctors in das functionoid eingefroren werden, wenn Sie das Array ofunctionoids erstellen:
Wenn also der Benutzer doit()
eines dieser functionoids aufruft, liefert er die „restlichen“ args, und der Aufruf kombiniert konzeptionell die ursprünglichen args, die an den ctor übergeben wurden, mit denen, die an die doit()
-Methode übergeben wurden:
array->doit(12);
Wie ich bereits angedeutet habe, besteht einer der Vorteile von functionoids darin, dass man mehrere Instanzen von, sagen wir, Funct1
in seinem Array haben kann, und diese Instanzen können unterschiedliche Parameter enthalten. Zum Beispiel sind array
und array
beide vom Typ Funct1
, aber das Verhalten von array->doit(12)
wird sich vom Verhalten von array->doit(12)
unterscheiden, da das Verhalten sowohl von der 12 abhängt, die an doit()
übergeben wurde, als auch von den Args, die an die Koren übergeben wurden.
Ein weiterer Vorteil von Functionoids wird deutlich, wenn wir das Beispiel von einem Array von Functionoids zu einem lokalen Functionoid ändern. Um die Voraussetzungen zu schaffen, gehen wir zurück zum altmodischen Funktionszeiger-Ansatz und stellen uns vor, dass Sie versuchen, eine Vergleichsfunktion an eine sort()
– oder binarySearch()
-Routine zu übergeben. Die sort()
oder binarySearch()
Routine heißt childRoutine()
und der Typ des Vergleichs-Funktions-Zeigers heißt FunctPtr
:
void childRoutine(FunctPtr f){ // ... f( /*...args...*/ ); // ...}
Dann würden verschiedene Aufrufer verschiedene Funktions-Zeiger übergeben, je nachdem, was sie für das Beste halten:
void myCaller(){ // ... childRoutine(funct1); // ...}void yourCaller(){ // ... childRoutine(funct3); // ...}
Wir können dieses Beispiel leicht in ein Beispiel mit Functionoids übersetzen:
Anhand dieses Beispiels können wir zwei Vorteile von Functionoids gegenüber Funktionszeigern erkennen. Der oben beschriebene „ctor args“-Vorteil und die Tatsache, dass Functionoids den Zustand zwischen den Aufrufen auf eine thread-sichere Art und Weise aufrechterhalten können.Bei einfachen Funktionszeigern wird der Zustand zwischen den Aufrufen normalerweise über statische Daten aufrechterhalten. Statische Daten sind jedoch nicht grundsätzlich thread-sicher – statische Daten werden von allen Threads gemeinsam genutzt. Der Functionoid-Ansatz bietet Ihnen etwas, das von Haus aus thread-sicher ist, da der Code mit thread-lokalen Daten endet. Die Implementierung ist einfach: man ändert das altmodische statische Datenelement in ein Instanzdatenelement innerhalb des this
-Objekts des Functionoids, und schwupps sind die Daten nicht nur thread-lokal, sondern sogar sicher bei rekursiven Aufrufen: jeder Aufruf von yourCaller()
hat sein eigenes, eindeutiges Funct3
-Objekt mit seinen eigenen, eindeutigen Instanzdaten.
Beachte, dass wir etwas gewonnen haben, ohne etwas zu verlieren. Wenn man thread-globale Daten haben will, können functionoids das auch: Man muss sie nur von einem Instanzdatenelement innerhalb des this
-Objekts des functionoids in ein statisches Datenelement innerhalb der Klasse des functionoids oder sogar in ein statisches Datenelement mit lokalem Umfang ändern. Man wäre nicht besser dran als mit Funktionszeigern, aber auch nicht schlechter.
Der Functionoid-Ansatz bietet eine dritte Option, die mit dem altmodischen Ansatz nicht verfügbar ist: Das Functionoid lässt den Aufrufer entscheiden, ob er thread-lokale oder thread-globale Daten will. In den Fällen, in denen sie thread-globale Daten wollen, sind sie für die Verwendung von Locks verantwortlich, aber zumindest haben sie die Wahl. Es ist einfach:
Funktionsoide lösen nicht jedes Problem, das bei der Entwicklung flexibler Software auftritt, aber sie sind deutlich leistungsfähiger als Funktionszeiger und sollten zumindest evaluiert werden. In der Tat kann man leicht beweisen, dass Functionoids gegenüber Funktionszeigern nicht weniger leistungsfähig sind, da man sich vorstellen kann, dass der altmodische Ansatz der Funktionszeiger gleichbedeutend mit einem globalen(!) Functionoid-Objekt ist. Da man immer ein globales functionoid-Objekt erstellen kann, hat man keinen Grund verloren. QED.
Kann man Functionoids schneller machen als normale Funktionsaufrufe?
Ja.
Wenn man ein kleines Functionoid hat, und das ist in der realen Welt ziemlich häufig, können die Kosten des Funktionsaufrufs hoch sein im Vergleich zu den Kosten der Arbeit, die das Functionoid leistet. In der vorherigen FAQ wurden Funktionoide mit Hilfe von virtuellen Funktionen implementiert und kosten typischerweise einen Funktionsaufruf. Ein alternativer Ansatz verwendet Vorlagen.
Das folgende Beispiel ähnelt dem in der vorherigen FAQ. Ich habe doit()
inoperator()()
umbenannt, um die Lesbarkeit des Aufrufer-Codes zu verbessern und um jemandem die Möglichkeit zu geben, einen regulären Funktions-Zeiger zu übergeben:
Der Unterschied zwischen diesem Ansatz und dem in der vorherigen FAQ ist, dass das Funktionsoid zur Kompilierzeit an den Aufrufer „gebunden“ wird und nicht zur Laufzeit. Stellen Sie sich das wie die Übergabe eines Parameters vor: Wenn Sie zur Kompilierzeit wissen, welche Art von Funktionoid Sie letztendlich übergeben wollen, dann können Sie die obige Technik verwenden, und Sie können, zumindest in typischen Fällen, einen Geschwindigkeitsvorteil daraus ziehen, dass der Compiler den Funktionoid-Code innerhalb des Aufrufers in eine Zeile expandiert. Hier ein Beispiel:
template <typename FunctObj>void myCode(FunctObj f){ // ... f( /*...args-go-here...*/ ); // ...}
Wenn der Compiler den obigen Code kompiliert, könnte er den Aufruf inline-expandieren, was die Leistung verbessern könnte.
Hier ist eine Möglichkeit, das obige aufzurufen:
void blah(){ // ... Funct2 x("functionoids are powerful", 42); myCode(x); // ...}
Außerdem: wie im ersten Absatz oben angedeutet wurde, können Sie auch die Namen normaler Funktionen übergeben (obwohl Sie die Kosten des Funktionsaufrufs tragen könnten, wenn der Aufrufer diese verwendet):
void myNormalFunction(int x);void blah(){ // ... myCode(myNormalFunction); // ...}
Was ist der Unterschied zwischen einem functionoid und einem functor?
Ein functionoid ist ein Objekt, das eine Hauptmethode hat. Es ist im Grunde die OO-Erweiterung einer C-ähnlichen Funktion wieprintf(). Man würde ein functionoid immer dann verwenden, wenn die Funktion mehr als einen Einstiegspunkt hat (d.h. mehr als eine „Methode“) und/oder den Zustand zwischen den Aufrufen auf eine thread-sichere Art und Weise aufrechterhalten muss (der C-ähnliche Ansatz, den Zustand zwischen den Aufrufen aufrechtzuerhalten, besteht darin, der Funktion eine lokale „statische“ Variable hinzuzufügen, aber das ist in einer Multi-Thread-Umgebung schrecklich unsicher).
Ein Funktor ist ein Spezialfall eines functionoids: Es ist ein functionoid, dessen Methode der „Funktionsaufruf-Operator“ operator()() ist. Da er den Funktionsaufruf-Operator überlädt, kann der Code seine Hauptmethode mit der gleichen Syntax aufrufen, die er für einen Funktionsaufruf verwenden würde. Wenn z. B. „foo“ ein Funktor ist, würde man „foo()“ sagen, um die Methode „operator()()“ für das Objekt „foo“ aufzurufen. Dies hat den Vorteil, dass die Schablone einen Schablonenparameter haben kann, der als Funktion verwendet wird, und dieser Parameter kann entweder der Name einer Funktion oder eines Funktor-Objekts sein. Es hat einen Leistungsvorteil, wenn es ein Funktor-Objekt ist, da die Methode „operator()()“ inlined sein kann (wohingegen die Übergabe der Adresse einer Funktion zwangsläufig non-inlined sein muss).
Dies ist sehr nützlich für Dinge wie die Funktion „comparison“ bei sortierten Containern. In C wird die Vergleichsfunktion immer per Zeiger übergeben (z.B., siehe die Signatur von „qsort()“), aber in C++ kann der Parameter entweder als Zeiger auf eine Funktion ODER als Name eines Funktor-Objekts übergeben werden, und das Ergebnis ist, dass sortierte Container in C++ in einigen Fällen sehr viel schneller (und niemals langsamer) sein können als das Äquivalent in C.
Da Java nichts Ähnliches wie Templates hat, muss es dynamische Bindung für all diese Dinge verwenden, und dynamische Bindung bedeutet notwendigerweise einen Funktionsaufruf. Normalerweise ist das keine große Sache, aber in C++ wollen wir extrem leistungsstarken Code ermöglichen. Das bedeutet, dass die Sprache niemals willkürlich einen Overhead auferlegen darf, der über das hinausgeht, was die physische Maschine leisten kann (natürlich kann ein Programmierer optional Techniken wie dynamische Bindung verwenden, die im Allgemeinen einen gewissen Overhead im Austausch für Flexibilität oder eine andere „Fähigkeit“ mit sich bringen, aber es ist Sache des Designers und des Programmierers zu entscheiden, ob sie die Vorteile (und Kosten) solcher Konstrukte nutzen wollen).
Leave a Reply