Appel de méthodes de classe C ++ via un pointeur de fonction

113

Comment obtenir un pointeur de fonction pour une fonction membre de classe et appeler ultérieurement cette fonction membre avec un objet spécifique? J'aimerais écrire:

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


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

De plus, si possible, j'aimerais également appeler le constructeur via un pointeur:

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

Est-ce possible, et si oui, quelle est la meilleure façon de procéder?

Tony le poney
la source
1
Je ne comprends toujours pas vraiment «pourquoi» si vous voulez appeler une fonction membre d'objets, passez simplement un pointeur vers l'objet? Si les gens se plaignent de cela parce que cela vous permet de mieux encapsuler la classe, pourquoi ne pas créer une classe d'interface dont toutes les classes héritent?
Chad
Cela peut être utile pour implémenter quelque chose comme le modèle de commande bien que beaucoup de gens utilisent boost :: function pour masquer la mécanique brute du pointeur de membre.
CB Bailey
9
Pourquoi attribuez-vous ce chien dynamiquement? Vous devez également supprimer manuellement l'objet. Cela ressemble beaucoup à ce que vous venez de Java, C # ou d'un autre langage comparable et que vous vous battez toujours avec C ++. Un objet automatique simple ( Dog dog;) est plus probablement ce que vous voulez.
sbi
1
@Chad: Je serais plutôt d'accord mais il y a des moments où passer une référence serait plus coûteux. Considérez une boucle qui itère sur certains types de données (analyse, calcul, etc.) que de pouvoir appeler une fonction basée sur certains calculs if / else impose un coût où le simple appel de la fonction pointée trop pourrait éviter un tel si / alors / else vérifie si ces vérifications peuvent être effectuées avant d'entrer dans la boucle.
Eric

Réponses:

127

Lisez ceci pour plus de détails:

// 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');
Satbir
la source
24
Étonnant qu'ils aient décidé que ceci: *this.*pt2Memberfonctionnerait. *a une priorité plus élevée sur .*... Personnellement, j'aurais quand même écrit this->*pt2Member, c'est un opérateur de moins.
Alexis Wilke
7
Pourquoi avez-vous d'initialiser pt2ConstMemberà NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@AlexisWilke pourquoi est-ce surprenant? Pour les objets directs (et non les pointeurs) c'est le cas (object.*method_pointer), nous voulons donc *qu'ils aient une plus grande priorité.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@ TomášZato, si je ne me trompe pas (et je pourrais l'être), thisest juste utilisé pour démontrer que tout ce que vous appliquez .*doit être un pointeur vers une instance de la (sous) classe. Cependant, c'est une nouvelle syntaxe pour moi, je ne fais que deviner sur la base d'autres réponses et ressources liées ici. Je suggère une modification pour rendre cela plus clair.
c1moore
1
Oh snap, félicitations pour 100!
Jonathan Mee
58

Comment obtenir un pointeur de fonction pour une fonction membre de classe et appeler ultérieurement cette fonction membre avec un objet spécifique?

Il est plus simple de commencer par un fichier typedef. Pour une fonction membre, vous ajoutez le nom de classe dans la déclaration de type:

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

Ensuite, pour appeler la méthode, vous utilisez l' ->*opérateur:

(pDog->*pBark)();

Aussi, si possible, j'aimerais également invoquer le constructeur via un pointeur. Est-ce possible, et si oui, quelle est la meilleure façon de procéder?

Je ne crois pas que vous puissiez travailler avec des constructeurs comme celui-ci - les ctors et les dtors sont spéciaux. La manière normale de réaliser ce genre de chose serait d'utiliser une méthode de fabrique, qui est essentiellement une fonction statique qui appelle le constructeur pour vous. Voir le code ci-dessous pour un exemple.

J'ai modifié votre code pour faire essentiellement ce que vous décrivez. Il y a quelques mises en garde ci-dessous.

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

Maintenant, bien que vous puissiez normalement utiliser a Dog*à la place d'un Animal*grâce à la magie du polymorphisme, le type d'un pointeur de fonction ne suit pas les règles de recherche de la hiérarchie des classes. Ainsi, un pointeur de méthode Animal n'est pas compatible avec un pointeur de méthode Dog, en d'autres termes vous ne pouvez pas affecter a Dog* (*)()à une variable de type Animal* (*)().

La newDogméthode statique est un exemple simple de fabrique, qui crée et renvoie simplement de nouvelles instances. Étant une fonction statique, elle a un régulier typedef(sans qualificatif de classe).

Ayant répondu à ce qui précède, je me demande s'il n'y a pas de meilleur moyen d'atteindre ce dont vous avez besoin. Il existe quelques scénarios spécifiques dans lesquels vous feriez ce genre de chose, mais vous pourriez trouver d'autres modèles qui fonctionnent mieux pour votre problème. Si vous décrivez en termes plus généraux ce que vous essayez d'accomplir, l'esprit-ruche peut s'avérer encore plus utile!

En relation avec ce qui précède, vous trouverez sans aucun doute la bibliothèque de liaison Boost et d'autres modules connexes très utiles.

gavinb
la source
10
J'utilise C ++ depuis plus de 10 ans et j'apprends régulièrement quelque chose de nouveau. Je n'avais jamais entendu parler ->*auparavant, mais maintenant j'espère que je n'en aurai plus jamais besoin :)
Thomas
31

Je ne pense pas que quiconque ait expliqué ici qu'un problème est que vous avez besoin de " pointeurs membres " plutôt que de pointeurs de fonction normaux.

Les pointeurs de membres vers des fonctions ne sont pas simplement des pointeurs de fonction. En termes d'implémentation, le compilateur ne peut pas utiliser une adresse de fonction simple car, en général, vous ne connaissez pas l'adresse à appeler tant que vous ne savez pas pour quel objet déréférencer (pensez aux fonctions virtuelles). Vous devez également connaître l'objet afin de fournir le thisparamètre implicite, bien sûr.

Cela dit, vous en avez besoin, je dirai maintenant que vous devez vraiment les éviter. Sérieusement, les pointeurs de membres sont une douleur. Il est beaucoup plus sensé de regarder des modèles de conception orientés objet qui atteignent le même objectif, ou d'utiliser un boost::functionou quoi que ce soit comme mentionné ci-dessus - en supposant que vous puissiez faire ce choix, c'est-à-dire.

Si vous fournissez ce pointeur de fonction vers du code existant, vous avez donc vraiment besoin d' un simple pointeur de fonction, vous devez écrire une fonction en tant que membre statique de la classe. Une fonction membre statique ne comprend pas this, vous devrez donc transmettre l'objet en tant que paramètre explicite. Il y avait une fois un idiome pas si inhabituel dans ce sens pour travailler avec l'ancien code C qui a besoin de pointeurs de fonction

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

    static void myfunction (myclass *p);
}

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

Puisqu'il myfunctions'agit en réalité d'une fonction normale (mis à part les problèmes de portée), un pointeur de fonction peut être trouvé de la manière C normale.

EDIT - ce type de méthode est appelé "méthode de classe" ou "fonction membre statique". La principale différence avec une fonction non membre est que, si vous la référencez depuis l'extérieur de la classe, vous devez spécifier l'étendue à l'aide de l' ::opérateur de résolution d'étendue. Par exemple, pour obtenir le pointeur de fonction, utilisez &myclass::myfunctionet pour l'appeler, utilisez myclass::myfunction (arg);.

Ce genre de chose est assez courant lors de l'utilisation des anciennes API Win32, qui étaient à l'origine conçues pour C plutôt que C ++. Bien sûr, dans ce cas, le paramètre est normalement LPARAM ou similaire plutôt qu'un pointeur, et une conversion est nécessaire.

Steve314
la source
«myfunction» n'est pas une fonction normale si par normal vous entendez une fonction de style C. «myfunction» est plus précisément appelée une méthode de myclass. Les méthodes d'une classe ne sont pas comme les fonctions normales en ce sens qu'elles ont quelque chose qu'une fonction de style C n'a pas, qui est le pointeur «this».
Eric
3
conseiller d'utiliser boost est draconien. Il existe de bonnes raisons pratiques d'utiliser des pointeurs de méthode. Cela ne me dérange pas de mentionner le boost comme alternative, mais je déteste quand quelqu'un dit que quelqu'un d'autre devrait l'utiliser sans connaître tous les faits. Boost a un coût! Et s'il s'agit d'une plate-forme intégrée, ce n'est peut-être pas un choix possible. Au-delà de cela, j'aime vraiment votre rédaction.
Eric
@Eric - Sur votre deuxième point, je n'avais pas l'intention de dire "tu utiliseras Boost", et en fait je n'ai jamais utilisé Boost moi-même. L'intention (pour autant que je sache après 3 ans) était que les gens devraient chercher des alternatives et énumérer quelques possibilités. «Ou quoi que ce soit» indique qu'une liste n'est pas censée être exhaustive. Les pointeurs de membre ont un coût en lisibilité. Leur représentation source concise peut également masquer les coûts d'exécution - en particulier, un pointeur membre vers une méthode doit gérer à la fois des méthodes non virtuelles et virtuelles, et doit savoir lesquelles.
Steve314
@Eric - Non seulement cela, mais ces problèmes sont une raison de non-portabilité avec les pointeurs membres - Visual C ++, au moins dans le passé, avait besoin d'indices supplémentaires sur la façon de représenter les types de pointeurs membres. J'utiliserais l'approche de la fonction statique pour un système embarqué - la représentation d'un pointeur est la même que celle de tout autre pointeur de fonction, les coûts sont évidents et il n'y a pas de problème de portabilité. Et l'appel encapsulé par la fonction membre statique sait (au moment de la compilation) si l'appel est virtuel ou non - aucune vérification au moment de l'exécution n'est nécessaire au-delà des recherches habituelles de vtable pour les méthodes virtuelles.
Steve314
@Eric - sur votre premier point - je suis conscient qu'une fonction membre statique n'est pas exactement la même chose qu'une fonction de style C (d'où "les problèmes de portée mis à part"), mais j'aurais probablement dû inclure le nom.
Steve314
18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference
AraK
la source
2
Doit être: (pDog -> * doSomething) (); // si pDog est un pointeur // (pDog. * doSomething) (); // si pDog est une référence car l'opérateur () a une priorité plus élevée alors -> * et. *.
Tomek
12

Exemple exécutable minimal

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

Compilez et exécutez:

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

Testé dans Ubuntu 18.04.

Vous ne pouvez pas modifier l'ordre des parenthèses ou les omettre. Les éléments suivants ne fonctionnent pas:

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

GCC 9.2 échouerait avec:

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

Norme C ++ 11

.*et ->*sont des opérateurs uniques introduits en C ++ à cet effet, et non présents en C.

Projet de norme C ++ 11 N3337 :

  • 2.13 «Opérateurs et ponctuateurs» a une liste de tous les opérateurs, qui contient .*et ->*.
  • 5.5 "Opérateurs pointeur vers membre" explique ce qu'ils font
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
11

Je suis venu ici pour apprendre à créer un pointeur de fonction (pas un pointeur de méthode) à partir d'une méthode, mais aucune des réponses ici ne fournit de solution. Voici ce que j'ai trouvé:

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>

Donc, pour votre exemple, vous feriez maintenant:

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

Edit:
En utilisant C ++ 17, il existe une solution encore meilleure:

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

qui peut être utilisé directement sans la macro:

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

Pour les méthodes avec des modificateurs comme constvous pourriez avoir besoin de plus de spécialisations comme:

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...);
    }
};
cil
la source
6

La raison pour laquelle vous ne pouvez pas utiliser de pointeurs de fonction pour appeler des fonctions membres est que les pointeurs de fonction ordinaires ne sont généralement que l'adresse mémoire de la fonction.

Pour appeler une fonction membre, vous devez savoir deux choses:

  • Quelle fonction membre appeler
  • Quelle instance doit être utilisée (dont la fonction membre)

Les pointeurs de fonction ordinaires ne peuvent pas stocker les deux. Les pointeurs de fonction membre C ++ sont utilisés pour stocker a), c'est pourquoi vous devez spécifier explicitement l'instance lors de l'appel d'un pointeur de fonction membre.

hrnt
la source
1
J'ai voté pour cela mais j'ajouterais un point de clarification au cas où le PO ne sait pas à quoi vous faites référence par "quelle instance". Je développerais pour expliquer le pointeur «ceci» inhérent.
Eric
6

Un pointeur de fonction vers un membre de classe est un problème qui convient vraiment à l'utilisation de boost :: function. Petit exemple:

#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);
}
Benjamin
la source
2

Pour créer un nouvel objet, vous pouvez soit utiliser placement new, comme mentionné ci-dessus, soit demander à votre classe d'implémenter une méthode clone () qui crée une copie de l'objet. Vous pouvez ensuite appeler cette méthode de clonage à l'aide d'un pointeur de fonction membre comme expliqué ci-dessus pour créer de nouvelles instances de l'objet. L'avantage du clone est que parfois vous travaillez avec un pointeur vers une classe de base où vous ne connaissez pas le type de l'objet. Dans ce cas, une méthode clone () peut être plus simple à utiliser. De plus, clone () vous permettra de copier l'état de l'objet si c'est ce que vous voulez.

Corwin Joy
la source
les clones peuvent être coûteux et l'OP peut souhaiter les éviter si les performances sont un problème ou un problème.
Eric
0

J'ai fait cela avec std :: function et std :: bind ..

J'ai écrit cette classe EventManager qui stocke un vecteur de gestionnaires dans un unordered_map qui mappe les types d'événement (qui sont simplement const unsigned int, j'ai une grande énumération à la portée de l'espace de noms) à un vecteur de gestionnaires pour ce type d'événement.

Dans ma classe EventManagerTests, j'ai configuré un gestionnaire d'événements, comme ceci:

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

Voici la fonction 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;
}

Voici la définition du type EventHandler:

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

Puis de retour dans EventManagerTests :: RaiseEvent, je fais ceci:

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

Voici le code pour 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);
            }
        );
    }
}

Cela marche. Je reçois l'appel dans EventManagerTests :: OnKeyDown. Je dois supprimer les vecteurs au moment du nettoyage, mais une fois que je le fais, il n'y a pas de fuites. Le déclenchement d'un événement prend environ 5 microsecondes sur mon ordinateur, ce qui est vers 2008. Pas vraiment très rapide, mais. Aussi longtemps que je le sais et que je ne l'utilise pas dans du code ultra chaud.

J'aimerais l'accélérer en lançant mes propres std :: function et std :: bind, et peut-être en utilisant un tableau de tableaux plutôt qu'une unordered_map de vecteurs, mais je n'ai pas tout à fait compris comment stocker une fonction membre pointeur et appelez-le à partir d'un code qui ne sait rien de la classe appelée. La réponse de Eyelash semble très intéressante.

Shavais
la source