Chiamare metodi di classe C ++ tramite un puntatore a funzione


113

Come ottengo un puntatore a funzione per una funzione membro di classe e in seguito chiamo quella funzione membro con un oggetto specifico? Vorrei scrivere:

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


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

Inoltre, se possibile, vorrei invocare il costruttore anche tramite un puntatore:

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

È possibile e, in tal caso, qual è il modo migliore per farlo?


1
Ancora non capisco veramente il "perché" se si desidera chiamare una funzione membro di un oggetto, passare semplicemente un puntatore all'oggetto? Se le persone si lamentano di questo perché ti consente di incapsulare meglio la classe, perché non creare una classe di interfaccia da cui ereditano tutte le classi?
Ciad,

Può essere utile nell'implementazione di qualcosa come il modello di comando sebbene molte persone userebbero boost :: function per nascondere la meccanica del puntatore del membro grezzo.
CB Bailey

9
Perché assegni quel cane in modo dinamico? È quindi necessario eliminare manualmente anche l'oggetto. Sembra che tu provenga da Java, C # o qualche altro linguaggio comparabile e continui a combattere con C ++. Un semplice oggetto automatico ( Dog dog;) è più probabile quello che vuoi.
sbi

1
@Chad: sarei per lo più d'accordo, ma ci sono momenti in cui passare un riferimento sarebbe più costoso. Considera un ciclo che sta iterando su un certo tipo di dati (analisi, calcolo, ecc.) Che essere in grado di chiamare una funzione basata su alcuni calcoli if / else impone un costo in cui solo chiamando la funzione puntata troppo potrebbe evitare tale if / then / else controlla se questi controlli possono essere eseguiti prima di entrare nel ciclo.
Eric,

Risposte:


126

Leggi questo per i dettagli:

// 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
Sorprendente che abbiano deciso che questo: *this.*pt2Memberavrebbe funzionato. *ha la precedenza maggiore su .*... Personalmente, avrei comunque scritto this->*pt2Member, è un operatore in meno.
Alexis Wilke

7
Perché devi inizializzare pt2ConstMembera NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@AlexisWilke perché è sorprendente? Per gli oggetti diretti (non i puntatori) lo è (object.*method_pointer), quindi vogliamo *che abbia una priorità maggiore.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@ TomášZato, se non sbaglio (e potrei esserlo), thisviene usato solo per dimostrare che qualunque cosa tu applichi .*dovrebbe essere un puntatore a un'istanza della (sotto) classe. Tuttavia questa è una nuova sintassi per me, sto solo supponendo in base ad altre risposte e risorse collegate qui. Sto suggerendo una modifica per renderlo più chiaro.
c1moore

1
Oh cavolo, congratulazioni per 100!
Jonathan Mee

57

Come ottengo un puntatore a funzione per una funzione membro di classe e in seguito chiamo quella funzione membro con un oggetto specifico?

È più facile iniziare con un file typedef. Per una funzione membro, aggiungi il nome della classe nella dichiarazione del tipo:

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

Quindi per invocare il metodo, usi l' ->*operatore:

(pDog->*pBark)();

Inoltre, se possibile, vorrei invocare anche il costruttore tramite un puntatore. È possibile e, in tal caso, qual è il modo migliore per farlo?

Non credo che tu possa lavorare con costruttori come questo: i medici ei medici sono speciali. Il modo normale per ottenere questo genere di cose sarebbe usare un metodo factory, che è fondamentalmente solo una funzione statica che chiama il costruttore per te. Vedere il codice seguente per un esempio.

Ho modificato il tuo codice per fare sostanzialmente quello che descrivi. Ci sono alcuni avvertimenti di seguito.

#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;
}

Anche se normalmente si può usare un Dog*al posto di un Animal*grazie alla magia del polimorfismo, il tipo di un puntatore a funzione non segue le regole di ricerca della gerarchia di classi. Quindi un puntatore al metodo Animal non è compatibile con un puntatore al metodo Dog, in altre parole non è possibile assegnare Dog* (*)()a una variabile di tipo Animal* (*)().

Il newDogmetodo statico è un semplice esempio di factory, che crea e restituisce semplicemente nuove istanze. Essendo una funzione statica, ha un regolare typedef(senza qualificatore di classe).

Avendo risposto a quanto sopra, mi chiedo se non ci sia un modo migliore per ottenere ciò di cui hai bisogno. Ci sono alcuni scenari specifici in cui faresti questo genere di cose, ma potresti scoprire che ci sono altri schemi che funzionano meglio per il tuo problema. Se descrivi in ​​termini più generali ciò che stai cercando di ottenere, la mente-alveare potrebbe rivelarsi ancora più utile!

In relazione a quanto sopra, troverai senza dubbio molto utili la libreria di bind Boost e altri moduli correlati.


10
Uso C ++ da oltre 10 anni e continuo a imparare qualcosa di nuovo regolarmente. Non ne avevo mai sentito parlare ->*prima, ma ora spero di non averne mai bisogno :)
Thomas,

31

Non credo che nessuno abbia spiegato qui che un problema è che hai bisogno di " puntatori membri " piuttosto che normali puntatori a funzione.

I puntatori dei membri alle funzioni non sono semplicemente puntatori alle funzioni. In termini di implementazione, il compilatore non può utilizzare un semplice indirizzo di funzione perché, in generale, non conosci l'indirizzo da chiamare finché non sai per quale oggetto dereferenziare (pensa alle funzioni virtuali). È inoltre necessario conoscere l'oggetto per fornire il thisparametro implicito, ovviamente.

Avendo detto che ne hai bisogno, ora dirò che hai davvero bisogno di evitarli. Seriamente, i suggerimenti dei membri sono un dolore. È molto più sensato guardare ai modelli di progettazione orientati agli oggetti che raggiungono lo stesso obiettivo, o usare un boost::functiono qualsiasi altra cosa come menzionato sopra, supponendo che tu possa fare quella scelta, cioè.

Se stai fornendo quel puntatore a funzione al codice esistente, quindi hai davvero bisogno di un semplice puntatore a funzione, dovresti scrivere una funzione come membro statico della classe. Una funzione membro statico non comprende this, quindi dovrai passare l'oggetto come parametro esplicito. C'era una volta un idioma non così insolito in questo senso per lavorare con il vecchio codice C che necessita di puntatori a funzione

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

    static void myfunction (myclass *p);
}

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

Poiché myfunctionè in realtà solo una funzione normale (a parte i problemi di ambito), un puntatore a funzione può essere trovato nel normale modo C.

EDIT - questo tipo di metodo è chiamato "metodo di classe" o "funzione membro statico". La differenza principale rispetto a una funzione non membro è che, se si fa riferimento a essa dall'esterno della classe, è necessario specificare l'ambito utilizzando l' ::operatore di risoluzione dell'ambito. Ad esempio, per ottenere il puntatore a funzione, usa &myclass::myfunctione per chiamarlo use myclass::myfunction (arg);.

Questo genere di cose è abbastanza comune quando si utilizzano le vecchie API Win32, originariamente progettate per C anziché per C ++. Ovviamente in quel caso, il parametro è normalmente LPARAM o simile piuttosto che un puntatore, e sono necessari alcuni casting.


"miafunzione" non è una funzione normale se per normale intendi una funzione in stile C. 'myfunction' è più precisamente chiamato un metodo di myclass. I metodi di una classe non sono come le normali funzioni in quanto hanno qualcosa che una funzione in stile C non ha, che è il puntatore "this".
Eric

3
consigliare di usare boost è draconiano. Esistono buone ragioni pratiche per utilizzare i puntatori ai metodi. Non mi dispiace menzionare il boost come alternativa, ma odio quando qualcuno dice che qualcun altro dovrebbe usarlo senza conoscere tutti i fatti. Il potenziamento ha un costo! E se questa è una piattaforma incorporata, potrebbe non essere una scelta possibile. Oltre a questo, mi piace molto il tuo articolo.
Eric

@Eric - Sul tuo secondo punto, non avevo intenzione di dire "tu userai Boost", e infatti non ho mai usato Boost me stesso. L'intenzione (per quanto ne so dopo 3 anni) era che le persone cercassero alternative e elencassero alcune possibilità. "O qualunque cosa" indica che un elenco non deve essere esaustivo. I puntatori dei membri hanno un costo in termini di leggibilità. La loro rappresentazione concisa della sorgente può anche mascherare i costi di runtime: in particolare un puntatore di un membro a un metodo deve far fronte sia a metodi non virtuali che virtuali e deve sapere quali.
Steve314

@Eric - Non solo, ma questi problemi sono una ragione per la non portabilità con i puntatori dei membri - Visual C ++, almeno in passato, necessitava di alcuni indizi extra su come rappresentare i tipi di puntatori dei membri. Userei l'approccio della funzione statica per un sistema integrato: la rappresentazione di un puntatore è la stessa di qualsiasi altro puntatore a funzione, i costi sono evidenti e non ci sono problemi di portabilità. E la chiamata racchiusa dalla funzione membro statica sa (in fase di compilazione) se la chiamata è virtuale o meno - non sono necessari controlli di runtime oltre alle solite ricerche vtable per metodi virtuali.
Steve314

@Eric - sul tuo primo punto - sono consapevole del fatto che una funzione membro statica non è esattamente la stessa di una funzione in stile C (da qui "problemi di ambito a parte"), ma probabilmente avrei dovuto includere il nome.
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
Dovrebbe essere: (pDog -> * doSomething) (); // se pDog è un puntatore // (pDog. * doSomething) (); // se pDog è un riferimento come operatore () ha una priorità più alta, quindi -> * e. *.
Tomek

12

Esempio minimo eseguibile

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);
}

Compila ed esegui:

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

Testato in Ubuntu 18.04.

Non è possibile modificare l'ordine delle parentesi o ometterle. Quanto segue non funziona:

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

GCC 9.2 fallirebbe con:

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);
      |

Standard C ++ 11

.*e ->*sono un unico operatore introdotto in C ++ per questo scopo, e non presente in C.

Bozza standard C ++ 11 N3337 :

  • 2.13 "Operatori e segni di punteggiatura" ha un elenco di tutti gli operatori, che contiene .*e ->*.
  • 5.5 "Operatori puntatore-membro" spiega cosa fanno

11

Sono venuto qui per imparare come creare un puntatore a funzione (non un puntatore a metodo) da un metodo, ma nessuna delle risposte qui fornisce una soluzione. Ecco cosa mi è venuto in mente:

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>

Quindi per il tuo esempio ora dovresti fare:

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

Modifica:
utilizzando C ++ 17, esiste una soluzione ancora migliore:

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...);
    }
};

che può essere utilizzato direttamente senza la macro:

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

Per metodi con modificatori come constpotresti aver bisogno di altre specializzazioni come:

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

Il motivo per cui non è possibile utilizzare i puntatori a funzione per chiamare funzioni membro è che i normali puntatori a funzione di solito sono solo l'indirizzo di memoria della funzione.

Per chiamare una funzione membro, devi sapere due cose:

  • Quale funzione membro chiamare
  • Quale istanza deve essere utilizzata (la cui funzione membro)

I puntatori a funzioni ordinarie non possono memorizzare entrambi. I puntatori a funzione membro C ++ vengono utilizzati per memorizzare a), motivo per cui è necessario specificare esplicitamente l'istanza quando si chiama un puntatore a funzione membro.


1
Ho votato a favore ma aggiungerei un punto di chiarimento nel caso in cui l'OP non sappia a cosa ti riferisci per "quale istanza". Vorrei espandere per spiegare il puntatore intrinseco "questo".
Eric

6

Un puntatore a funzione a un membro della classe è un problema che è veramente adatto all'uso di boost :: function. Piccolo esempio:

#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

Per creare un nuovo oggetto puoi usare il posizionamento new, come menzionato sopra, o fare in modo che la tua classe implementi un metodo clone () che crea una copia dell'oggetto. È quindi possibile chiamare questo metodo clone utilizzando un puntatore a funzione membro come spiegato sopra per creare nuove istanze dell'oggetto. Il vantaggio del clone è che a volte potresti lavorare con un puntatore a una classe base di cui non conosci il tipo di oggetto. In questo caso un metodo clone () può essere più facile da usare. Inoltre, clone () ti permetterà di copiare lo stato dell'oggetto se è quello che vuoi.


i cloni possono essere costosi e l'OP potrebbe desiderare di evitarli se le prestazioni sono un problema o un problema.
Eric

0

L'ho fatto con std :: function e std :: bind ..

Ho scritto questa classe EventManager che memorizza un vettore di gestori in un unordered_map che mappa i tipi di eventi (che sono solo const unsigned int, ne ho una grande enumerazione con ambito spazio dei nomi) a un vettore di gestori per quel tipo di evento.

Nella mia classe EventManagerTests, ho impostato un gestore di eventi, come questo:

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

Ecco la funzione AddEventListener:

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;
}

Ecco la definizione del tipo EventHandler:

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

Quindi di nuovo in EventManagerTests :: RaiseEvent, faccio questo:

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

Ecco il codice per 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);
            }
        );
    }
}

Funziona. Ricevo la chiamata in EventManagerTests :: OnKeyDown. Devo eliminare i vettori al momento della pulizia, ma una volta fatto non ci sono perdite. La generazione di un evento richiede circa 5 microsecondi sul mio computer, che è circa il 2008. Non esattamente super veloce, ma. Abbastanza giusto finché lo so e non lo uso in codice ultra caldo.

Mi piacerebbe velocizzarlo eseguendo il rollio dei miei std :: function e std :: bind, e magari usando un array di array piuttosto che un unordered_map di vettori, ma non ho ancora capito come memorizzare una funzione membro pointer e chiamalo da codice che non sa nulla della classe chiamata. La risposta di Eyelash sembra molto interessante ..

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.