Aufruf von C ++ - Klassenmethoden über einen Funktionszeiger


113

Wie erhalte ich einen Funktionszeiger für eine Klassenelementfunktion und rufe diese Elementfunktion später mit einem bestimmten Objekt auf? Ich würde gerne schreiben:

class Dog : Animal
{
    Dog ();
    void bark ();
}


Dog* pDog = new Dog ();
BarkFunction pBark = &Dog::bark;
(*pBark) (pDog);

Wenn möglich, möchte ich den Konstruktor auch über einen Zeiger aufrufen:

NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

Ist dies möglich und wenn ja, wie wird dies bevorzugt?


1
Ich verstehe immer noch nicht wirklich, warum. Wenn Sie eine Objektmitgliedsfunktion aufrufen möchten, übergeben Sie einfach einen Zeiger auf das Objekt. Wenn sich die Leute darüber beschweren, weil Sie damit die Klasse besser kapseln können, warum nicht eine Schnittstellenklasse erstellen, von der alle Klassen erben?
Tschad

Es kann nützlich sein, um so etwas wie das Befehlsmuster zu implementieren, obwohl viele Leute die boost :: -Funktion verwenden würden, um die Mechanik des rohen Elementzeigers zu verbergen.
CB Bailey

9
Warum ordnen Sie diesen Hund dynamisch zu? Sie müssen das Objekt dann auch manuell löschen. Das sieht sehr danach aus, als ob Sie aus Java, C # oder einer anderen vergleichbaren Sprache kommen und immer noch mit C ++ kämpfen. Ein einfaches automatisches Objekt ( Dog dog;) ist eher das, was Sie wollen.
sbi

1
@Chad: Ich würde größtenteils zustimmen, aber es gibt Zeiten, in denen das Übergeben einer Referenz teurer wäre. Stellen Sie sich eine Schleife vor, die über eine Art von Daten (Parsen, Berechnen usw.) iteriert, als wenn Sie eine Funktion auf der Grundlage einiger if / else-Berechnungen aufrufen können, die Kosten verursacht, bei denen nur das Aufrufen der Funktion "point to" eine solche if / then-Funktion vermeiden könnte / else prüft, ob diese Prüfungen durchgeführt werden können, bevor die Schleife betreten wird.
Eric

Antworten:


126

Lesen Sie dies für Details:

// 1 define a function pointer and initialize to NULL

int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;

// C++

class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* more of TMyClass */
};
pt2ConstMember = &TMyClass::DoIt; // note: <pt2Member> may also legally point to &DoMore

// Calling Function using Function Pointer

(*this.*pt2ConstMember)(12, 'a', 'b');

23
Überraschend, dass sie beschlossen, dass dies: *this.*pt2Memberfunktionieren würde. *hat einen höheren Vorrang vor .*... Persönlich hätte ich immer noch geschrieben this->*pt2Member, das ist ein Operator weniger.
Alexis Wilke

7
Warum müssen Sie initialisieren pt2ConstMemberzu NULL?
Ciro Santilli 1 冠状 病 六四 事件 法轮功

@AlexisWilke warum ist es überraschend? Für direkte Objekte (keine Zeiger) ist (object.*method_pointer)dies der Fall, daher möchten wir, dass die Objekte *eine höhere Priorität haben.
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功

@ TomášZato, wenn ich mich nicht irre (und ich könnte es auch sein), thiswird nur verwendet, um zu demonstrieren, dass alles, worauf Sie sich beziehen, .*ein Zeiger auf eine Instanz der (Unter-) Klasse sein sollte. Dies ist jedoch eine neue Syntax für mich, die ich nur aufgrund anderer Antworten und Ressourcen vermute, die hier verlinkt sind. Ich schlage eine Bearbeitung vor, um dies klarer zu machen.
c1moore

1
Oh Schnappschuss, herzlichen Glückwunsch zu 100!
Jonathan Mee

57

Wie erhalte ich einen Funktionszeiger für eine Klassenelementfunktion und rufe diese Elementfunktion später mit einem bestimmten Objekt auf?

Es ist am einfachsten, mit einem zu beginnen typedef. Für eine Mitgliedsfunktion fügen Sie den Klassennamen in die Typdeklaration ein:

typedef void(Dog::*BarkFunction)(void);

Um die Methode aufzurufen, verwenden Sie den ->*Operator:

(pDog->*pBark)();

Wenn möglich, möchte ich den Konstruktor auch über einen Zeiger aufrufen. Ist dies möglich und wenn ja, wie wird dies bevorzugt?

Ich glaube nicht, dass Sie mit solchen Konstruktoren arbeiten können - ctors und dtors sind etwas Besonderes. Der normale Weg, um so etwas zu erreichen, wäre die Verwendung einer Factory-Methode, die im Grunde nur eine statische Funktion ist, die den Konstruktor für Sie aufruft. Ein Beispiel finden Sie im folgenden Code.

Ich habe Ihren Code so geändert, dass er im Grunde das tut, was Sie beschreiben. Es gibt einige Einschränkungen unten.

#include <iostream>

class Animal
{
public:

    typedef Animal*(*NewAnimalFunction)(void);

    virtual void makeNoise()
    {
        std::cout << "M00f!" << std::endl;
    }
};

class Dog : public Animal
{
public:

    typedef void(Dog::*BarkFunction)(void);

    typedef Dog*(*NewDogFunction)(void);

    Dog () {}

    static Dog* newDog()
    {
        return new Dog;
    }

    virtual void makeNoise ()
    {
        std::cout << "Woof!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    // Call member function via method pointer
    Dog* pDog = new Dog ();
    Dog::BarkFunction pBark = &Dog::makeNoise;

    (pDog->*pBark)();

    // Construct instance via factory method
    Dog::NewDogFunction pNew = &Dog::newDog;

    Animal* pAnimal = (*pNew)();

    pAnimal->makeNoise();

    return 0;
}

Obwohl Sie dank der Magie des Polymorphismus normalerweise ein Dog*anstelle eines Animal*Dankes verwenden können, folgt der Typ eines Funktionszeigers nicht den Suchregeln der Klassenhierarchie. Ein Animal-Methodenzeiger ist also nicht mit einem Dog-Methodenzeiger kompatibel. Mit anderen Worten, Sie können Dog* (*)()einer Variablen vom Typ keine zuweisen Animal* (*)().

Die statische newDogMethode ist ein einfaches Beispiel für eine Factory, die einfach neue Instanzen erstellt und zurückgibt. Als statische Funktion hat sie eine reguläre Funktion typedef(ohne Klassenqualifikation).

Nachdem ich das oben Gesagte beantwortet habe, frage ich mich, ob es keinen besseren Weg gibt, um das zu erreichen, was Sie brauchen. Es gibt einige spezifische Szenarien, in denen Sie so etwas tun würden, aber möglicherweise finden Sie andere Muster, die für Ihr Problem besser geeignet sind. Wenn Sie allgemeiner beschreiben, was Sie erreichen möchten, kann sich der Hive-Mind als noch nützlicher erweisen!

In Bezug auf das oben Gesagte werden Sie die Boost-Bindungsbibliothek und andere verwandte Module zweifellos sehr nützlich finden.


10
Ich benutze C ++ seit über 10 Jahren und lerne regelmäßig etwas Neues. Ich hatte noch nie davon gehört ->*, aber jetzt hoffe ich, dass ich es nie brauchen werde :)
Thomas

31

Ich glaube nicht, dass hier jemand erklärt hat, dass ein Problem darin besteht, dass Sie " Mitgliedszeiger " anstelle von normalen Funktionszeigern benötigen .

Elementzeiger auf Funktionen sind nicht einfach Funktionszeiger. In Bezug auf die Implementierung kann der Compiler keine einfache Funktionsadresse verwenden, da Sie die aufzurufende Adresse im Allgemeinen erst kennen, wenn Sie wissen, für welches Objekt eine Dereferenzierung erforderlich ist (denken Sie an virtuelle Funktionen). thisNatürlich müssen Sie auch das Objekt kennen, um den impliziten Parameter bereitzustellen .

Nachdem ich gesagt habe, dass Sie sie brauchen, werde ich jetzt sagen, dass Sie sie wirklich vermeiden müssen. Im Ernst, Mitgliederzeiger sind ein Schmerz. Es ist viel vernünftiger, objektorientierte Entwurfsmuster zu betrachten, die das gleiche Ziel erreichen, oder ein boost::functionoder was auch immer wie oben erwähnt zu verwenden - vorausgesetzt, Sie können diese Wahl treffen, das heißt.

Wenn Sie diesen Funktionszeiger für vorhandenen Code bereitstellen, sodass Sie wirklich einen einfachen Funktionszeiger benötigen , sollten Sie eine Funktion als statisches Mitglied der Klasse schreiben. Eine statische Elementfunktion versteht das nicht this, daher müssen Sie das Objekt als expliziten Parameter übergeben. Es gab einmal eine nicht allzu ungewöhnliche Redewendung in dieser Richtung für die Arbeit mit altem C-Code, der Funktionszeiger benötigt

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

Da myfunctiones sich eigentlich nur um eine normale Funktion handelt (abgesehen von Umfangsproblemen), kann ein Funktionszeiger auf normale C-Weise gefunden werden.

BEARBEITEN - Diese Art von Methode wird als "Klassenmethode" oder "statische Elementfunktion" bezeichnet. Der Hauptunterschied zu einer Nichtmitgliedsfunktion besteht darin, dass Sie den Bereich mithilfe des ::Bereichsauflösungsoperators angeben müssen, wenn Sie von außerhalb der Klasse darauf verweisen . Um beispielsweise den Funktionszeiger abzurufen, verwenden Sie &myclass::myfunctionund rufen Sie ihn auf myclass::myfunction (arg);.

Diese Art von Dingen ist ziemlich häufig, wenn die alten Win32-APIs verwendet werden, die ursprünglich eher für C als für C ++ entwickelt wurden. In diesem Fall ist der Parameter natürlich normalerweise LPARAM oder ähnlich und kein Zeiger, und es ist ein gewisses Casting erforderlich.


'myfunction' ist keine normale Funktion, wenn Sie mit normal eine Funktion im C-Stil meinen. 'myfunction' wird genauer als myclass-Methode bezeichnet. Methoden einer Klasse sind insofern nicht wie normale Funktionen, als sie etwas haben, das eine Funktion im C-Stil nicht hat, nämlich den 'this'-Zeiger.
Eric

3
Die Verwendung von Boost ist drakonisch. Es gibt praktisch gute Gründe für die Verwendung von Methodenzeigern. Ich habe nichts dagegen, Boost als Alternative zu erwähnen, aber ich hasse es, wenn jemand sagt, jemand anderes sollte es verwenden, ohne alle Fakten zu kennen. Boost hat seinen Preis! Und wenn dies eine eingebettete Plattform ist, ist dies möglicherweise keine mögliche Wahl. Darüber hinaus mag ich Ihr Schreiben sehr.
Eric

@Eric - In deinem zweiten Punkt wollte ich nicht sagen "du sollst Boost verwenden", und tatsächlich habe ich Boost selbst nie verwendet. Die Absicht (soweit ich es nach 3 Jahren weiß) war, dass die Leute nach Alternativen suchen und einige Möglichkeiten auflisten sollten. "Oder was auch immer" bedeutet, dass eine Liste nicht vollständig sein soll. Mitgliederzeiger kosten Lesbarkeit. Ihre präzise Quellendarstellung kann auch Laufzeitkosten verschleiern - insbesondere muss ein Elementzeiger auf eine Methode sowohl mit nicht virtuellen als auch mit virtuellen Methoden umgehen und wissen, welche.
Steve314

@Eric - Nicht nur das, sondern diese Probleme sind auch ein Grund für die Nichtportabilität mit Elementzeigern. - Visual C ++ benötigte zumindest in der Vergangenheit einige zusätzliche Hinweise zur Darstellung von Elementzeigertypen. Ich würde den statischen Funktionsansatz für ein eingebettetes System verwenden - die Darstellung eines Zeigers ist dieselbe wie bei jedem anderen Funktionszeiger, die Kosten liegen auf der Hand und es gibt kein Portabilitätsproblem. Und der von der statischen Elementfunktion umschlossene Aufruf weiß (zur Kompilierungszeit), ob der Aufruf virtuell ist oder nicht - es sind keine Laufzeitprüfungen erforderlich, die über die üblichen vtable-Lookups für virtuelle Methoden hinausgehen.
Steve314

@Eric - zu Ihrem ersten Punkt - Ich bin mir bewusst, dass eine statische Elementfunktion nicht genau mit einer Funktion im C-Stil identisch ist (daher "Bereichsprobleme beiseite"), aber ich hätte wahrscheinlich den Namen einfügen sollen.
Steve314

18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference

2
Sollte sein: (pDog -> * doSomething) (); // wenn pDog ein Zeiger ist // (pDog. * doSomething) (); // Wenn pDog eine Referenz ist, da der Operator () eine höhere Priorität hat als -> * und. *.
Tomek

12

Minimal lauffähiges Beispiel

main.cpp

#include <cassert>

class C {
    public:
        int i;
        C(int i) : i(i) {}
        int m(int j) { return this->i + j; }
};

int main() {
    // Get a method pointer.
    int (C::*p)(int) = &C::m;

    // Create a test object.
    C c(1);
    C *cp = &c;

    // Operator .*
    assert((c.*p)(2) == 3);

    // Operator ->*
    assert((cp->*p)(2) == 3);
}

Kompilieren und ausführen:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Getestet in Ubuntu 18.04.

Sie können die Reihenfolge der Klammern nicht ändern oder weglassen. Folgendes funktioniert nicht:

c.*p(2)
c.*(p)(2)

GCC 9.2 würde fehlschlagen mit:

main.cpp: In function int main()’:
main.cpp:19:18: error: must use ‘.*’ or ‘->*’ to call pointer-to-member function in p (...)’, e.g. ‘(... ->* p) (...)’
   19 |     assert(c.*p(2) == 3);
      |

C ++ 11 Standard

.*und ->*sind einzelne Operatoren, die zu diesem Zweck in C ++ eingeführt wurden und in C nicht vorhanden sind.

C ++ 11 N3337 Standardentwurf :

  • 2.13 "Operatoren und Interpunktionszeichen" enthält eine Liste aller Operatoren, die .*und enthält ->*.
  • 5.5 "Zeiger-zu-Mitglied-Operatoren" erklärt, was sie tun

11

Ich bin hierher gekommen, um zu lernen, wie man aus einer Methode einen Funktionszeiger (keinen Methodenzeiger) erstellt, aber keine der Antworten hier bietet eine Lösung. Folgendes habe ich mir ausgedacht:

template <class T> struct MethodHelper;
template <class C, class Ret, class... Args> struct MethodHelper<Ret (C::*)(Args...)> {
    using T = Ret (C::*)(Args...);
    template <T m> static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

#define METHOD_FP(m) MethodHelper<decltype(m)>::call<m>

Für Ihr Beispiel würden Sie jetzt Folgendes tun:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

Bearbeiten:
Mit C ++ 17 gibt es eine noch bessere Lösung:

template <auto m> struct MethodHelper;
template <class C, class Ret, class... Args, Ret (C::*m)(Args...)> struct MethodHelper<m> {
    static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

die direkt ohne das Makro verwendet werden kann:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

Für Methoden mit Modifikatoren wie constbenötigen Sie möglicherweise weitere Spezialisierungen wie:

template <class C, class Ret, class... Args, Ret (C::*m)(Args...) const> struct MethodHelper<m> {
    static Ret call(const C* object, Args... args) {
        return (object->*m)(args...);
    }
};

6

Der Grund, warum Sie keine Funktionszeiger zum Aufrufen von Elementfunktionen verwenden können, ist, dass gewöhnliche Funktionszeiger normalerweise nur die Speicheradresse der Funktion sind.

Um eine Mitgliedsfunktion aufzurufen, müssen Sie zwei Dinge wissen:

  • Welche Mitgliedsfunktion soll aufgerufen werden?
  • Welche Instanz soll verwendet werden (deren Mitgliedsfunktion)

Gewöhnliche Funktionszeiger können nicht beide speichern. C ++ - Elementfunktionszeiger werden zum Speichern von a) verwendet. Aus diesem Grund müssen Sie die Instanz beim Aufrufen eines Elementfunktionszeigers explizit angeben.


1
Ich habe dies oben abgestimmt, würde aber einen Klarstellungspunkt hinzufügen, falls das OP nicht weiß, worauf Sie sich mit "welcher Instanz" beziehen. Ich würde erweitern, um den inhärenten 'this'-Zeiger zu erklären.
Eric

6

Ein Funktionszeiger auf ein Klassenmitglied ist ein Problem, das für die Verwendung der boost :: -Funktion wirklich geeignet ist. Kleines Beispiel:

#include <boost/function.hpp>
#include <iostream>

class Dog 
{
public:
   Dog (int i) : tmp(i) {}
   void bark ()
   {
      std::cout << "woof: " << tmp << std::endl;
   }
private:
   int tmp;
};



int main()
{
   Dog* pDog1 = new Dog (1);
   Dog* pDog2 = new Dog (2);

   //BarkFunction pBark = &Dog::bark;
   boost::function<void (Dog*)> f1 = &Dog::bark;

   f1(pDog1);
   f1(pDog2);
}

2

Um ein neues Objekt zu erstellen, können Sie entweder die oben erwähnte Platzierung new verwenden oder Ihre Klasse eine clone () -Methode implementieren lassen, die eine Kopie des Objekts erstellt. Sie können diese Klonmethode dann wie oben erläutert mit einem Elementfunktionszeiger aufrufen, um neue Instanzen des Objekts zu erstellen. Der Vorteil des Klons besteht darin, dass Sie manchmal mit einem Zeiger auf eine Basisklasse arbeiten, bei der Sie den Typ des Objekts nicht kennen. In diesem Fall kann eine clone () -Methode einfacher zu verwenden sein. Mit clone () können Sie außerdem den Status des Objekts kopieren, wenn Sie dies wünschen.


Klone können teuer sein und das OP möchte sie möglicherweise vermeiden, wenn die Leistung ein Problem oder ein Problem darstellt.
Eric

0

Ich habe das mit std :: function und std :: bind gemacht.

Ich habe diese EventManager-Klasse geschrieben, die einen Vektor von Handlern in einer ungeordneten_Map speichert, die Ereignistypen (die nur const unsigned int sind, ich habe eine große Anzahl von Namespace-Bereichen) einem Vektor von Handlern für diesen Ereignistyp zuordnet.

In meiner EventManagerTests-Klasse habe ich einen Ereignishandler wie folgt eingerichtet:

auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

Hier ist die AddEventListener-Funktion:

std::vector<EventHandler>::iterator EventManager::AddEventListener(EventType _event_type, EventHandler _handler)
{
    if (listeners_.count(_event_type) == 0) 
    {
        listeners_.emplace(_event_type, new std::vector<EventHandler>());
    }
    std::vector<EventHandler>::iterator it = listeners_[_event_type]->end();
    listeners_[_event_type]->push_back(_handler);       
    return it;
}

Hier ist die EventHandler-Typdefinition:

typedef std::function<void(Event *)> EventHandler;

Dann mache ich in EventManagerTests :: RaiseEvent Folgendes:

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

Hier ist der Code für EventManager :: RaiseEvent:

void EventManager::RaiseEvent(EventType _event_type, Event * _event)
{
    if (listeners_.count(_event_type) > 0)
    {
        std::vector<EventHandler> * vec = listeners_[_event_type];
        std::for_each(
            begin(*vec), 
            end(*vec), 
            [_event](EventHandler handler) mutable 
            {
                (handler)(_event);
            }
        );
    }
}

Das funktioniert. Ich erhalte den Aufruf in EventManagerTests :: OnKeyDown. Ich muss die Vektoren löschen, die Zeit zum Aufräumen haben, aber wenn ich das mache, gibt es keine Lecks. Das Auslösen eines Ereignisses dauert auf meinem Computer ungefähr 5 Mikrosekunden, was ungefähr 2008 entspricht. Nicht gerade superschnell, aber. Fair genug, solange ich das weiß und es nicht in extrem heißem Code verwende.

Ich möchte es beschleunigen, indem ich meine eigene std :: function und std :: bind rolle und vielleicht ein Array von Arrays anstelle einer ungeordneten Karte von Vektoren verwende, aber ich habe nicht ganz herausgefunden, wie eine Mitgliedsfunktion gespeichert werden soll Zeiger und rufen Sie es aus Code auf, der nichts über die aufgerufene Klasse weiß. Wimpern Antwort sieht sehr interessant aus ..

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.