Pointeurs sur fonctions membres?

Ca faisait longtemps que je n'étais pas venu vous embêter avec mes petits problèmes...

Voilà, dans un projet que j'ai en cours, je gère dans une boucle principale majoritairement trois objets.
Un bouton.
Un encodeur rotatif.
Et un objet qui génère des sons en fonction d'une interface utilisateur.

Cet objet qui gère des sons est défini dans une classe (Pad, StepSequencer et Bounce par exemple), qui hérite d'une classe abstraite SoundObject, ce qui me permet de définir un pointeur vers la classe abstraite, et d'appeler toujours les mêmes méthodes dans ma boucle prinicpale, même si l'objet change en fonction du mode choisi.

Jusque là ca va. L'encodeur rotatif est lu dans la boucle principale. Découvrant il y a quelques jours le concept de pointeurs sur fonction, je me suis dit que ce serait pratique de pouvoir attacher une fonction à l'encodeur. Comme ça je pourrais, en même temps que je change de "contexte" pour l'instrument utilisé, changer la manière dont réagit l'encodeur et lui faire exécuter une fonction dédiée.
Aussitôt dit aussitôt fait, je lui ai rattaché une méthode attach(), une méthode détach(), qui font très bien ce que l'on suppose qu'elles font, et une méthode exec() qui transmet l'état de l'encodeur à la fonction attachée.
Je suis surpris d'y être parvenue aussi bien aussi vite, ça fonctionne au poil... Avec des fonctions statiques.
D'ailleurs la bibliothèque est là, pour ceux que ça intéresse.

Maintenant j'aimerais assez pouvoir attacher une fonction membre d'une classe, mais ça bloque.
J'ai bien compris que ça ne fonctionne pas:

//Pointeur vers classe mère des objets gestionnaires de sons
SoundModule *module = NULL;
//Instanciation et définition de l'encodeur
Encoder wheel = Encoder();
wheel.begin(7, 8);

//instanciation, depuis le menu, d'un nouvel instrument, ici le step sequencer
Step *step = new Step();
//auquel je passe les références aux instanciations du synthétiseur, de l'interface graphique, et des notes réglées.
step->begin(&synth, &interface, &notes);
//et ici je le rattache au pointeur de la classe mère, ce qui me permet d'y avoir accès depuis la boucle principale via des méthodes communes.
module = step;

//et pour finir, je rattache la fonction désirée à mon encodeur
wheel.attach(step->updateSpeed);

Donc, ça ne marche pas. Je sais pourquoi, c'est parce que pour une fonction non statique, il faudrait que je remplace, dans la définition de l'encodeur, la première définition du pointeur par la deuxième, si j'ai bien compris.

//version actuelle, pour fonction statiques.
void (*_function)()

//ce qu'il faudrait, pour fonctions membres.
void (Step::*_function)()[

Or ça me pose un problème: je voulais pouvoir simplifier les choses avec ce pointeur sur fonction, mais si je dois définir un pointeur différent pour chaque type d'objet vers lequel il peut pointer, je pers tout la souplesse du système!
Je suppose que je pourrais aussi définir cette fonction comme une fonction virtuelle de ma classe SoundModule, mais d'une part tous les modules n'ont pas besoin de gérer l'encodeur, et d'autre part il faudrait aussi spécialiser la classe de l'encodeur.

Est-ce que vous savez si ce problème peut avoir une solution, ou bien si je tente de le contourner d'une manière ou l'autre? Jusque là je suis parvenu à conserver des sous-ensembles plutôt indépendants dans mon programme, j'amieria bien pouvoir continuer sur cette voie saine! :slight_smile:

Merci par avance!

Edit: NOTA: dans mon cas je pourrai probablement m'en sortir, parce que l'encodeur devrait intervenir sur des objets qui sont définis en début de programme, et restent accessible. Je pourrais donc écrire quelques fonctions statiques qui seraient dédiées à faire ces modifications. Mais ça reste quand même, il me semble, une manière de contourner le problème, le plus simple serait de pouvoir accéder directement aux objets concernés, quel que soit leur type.

J'ai eu ce même problème à résoudre il y a quelque temps. Voir ce sujet.

Comme pour moi, le problème n'était pas crucial (je n'avais qu'un seul objet à instancier) et que cela me paraissait compliqué dans l'environnement du compilateur de l'Arduino (AVR-GCC), j'ai abandonné. J'ai rendu statique les fonctions et variables concernées. Je sais, c'est pas joli :-\
Mais s'il existe une solution assez simple (et dans mes compétences), je suis preneur.

Cordialement.

Pierre

Ok, ça marche. J'ai cherché un peu sur google quand même, les tutos abordent presque toujours cette question de la spécificité des pointeurs vers fonction membre. Mais je n'ai pour l'instant rien trouvé sur ce point particulier où l'on veut rendre la chose "vraiment" dynamique.
Cela dit puisque ces fonctions sont des fonctions d'interface (genre changer le tempo, faire défiler l'écran latéralement ou de bas ne haut, etc.), et que l'interface, même si elle évolue reste disponible, j'ai fait quelques fonctions dédiées, et au lieu de les appeler depuis les modules (qui disposent chacun d'un pointeurs vers les tableaux de notes, l'interface et le synthétiseur) je les appelle depuis le main. En soi la différence n'est pas énorme, et c'est peut-être même plus propre ainsi.

Mais bon, je suis quand même preneur d'une solution, au moins pour ma culture, et parce que je suis presque sûr qu'en creusant sur les fonctionnalités qu'on pourrait ajouter à un module, il pourrait être nécessaire de pouvoir accéder directement à une fonction membre.

PS: j'essaierai de vous présenter le projet en question prochainement, mais il reste encore des choses à faire dessus. :slight_smile:

Bon, je viens de lire le fil de discussion dont tu m'as passé le lien, le moins que je puisse dire, c'est la même chose que toi: "ça me dépasse"!
J'ai compris dans les grandes lignes ce dont il était question, mais la syntaxe m'est trop peu familière pour que je comprenne les mécanismes en jeu. Donc j'y reviendrai. ça va faire son chemin dans la tête, mettre en place des petits signaux qui s'activeront lorsque je tomberai sur des choses similaires! :slight_smile:

En plus, du fait du compilateur spécifique à Arduino (AVR-GCC), on n'a pas accès à toutes les possibilités du C++.

Cordialement.

Pierre.

J'ai déjà tenté de t'expliquer comment éviter d'avoir besoin d'un pointeur sur fonction en C++ ici. Utiliser une classe d'interface t'évitera toujours d'avoir à recourir aux mécanismes et subterfuges des pointeurs sur fonction, qui est une fonctionnalité héritée du C, - presque - pas nécessaire en C++, sauf pour s'interfacer parfois avec du C...

Zorro_X:
J'ai déjà tenté de t'expliquer comment éviter d'avoir besoin d'un pointeur sur fonction en C++ ici. Utiliser une classe d'interface t'évitera toujours d'avoir à recourir aux mécanismes et subterfuges des pointeurs sur fonction, qui est une fonctionnalité héritée du C, - presque - pas nécessaire en C++, sauf pour s'interfacer parfois avec du C...

C'est vrai, mais ça m'avait passé au-dessus de la tête et comme j'avais réussi à résoudre mon problème avec des méthodes statiques (c'est pas beau, je sais), je n'avais pas insisté. Il faudrait que je m'y remette.

Cordialement.

Pierre

pepe:
....... Si plusieurs objets de classes différentes doivent être manipulés de la même manière, alors leurs classes peuvent dériver d'une classe d'objet commune au travers de laquelle ces manipulations seront effectuées sans faire apparaître les particularités de chacune (application des principes d'héritage et de polymorphisme).........

Bonsoir,

Je ne connais encore pas trop les arcanes des objets en C++, j'avais plus l'habitude de Delphi (qui est du Pascal objet nettement moins permissif que le C) mais je souscris entièrement à cette remarque pour le fond.

Le problème est juste que la construction de toute un hiérarchie de classes suppose que l'usage qui en sera fait ait été au préalable très soigneusement pensé.

Le" il y a des cadenas, c'est interdit" remplaçant le "Touche pas à ça p'tit con" .

Serge .D

Bon, je dois vous avouer que tout ça me dépasse. Mais je prendrai le temps de m'y pencher sérieusement, à un moment ou l'autre.

Donc ce serait possible, dans le cas de ma classe d'encodeur rotatif, de faire en sorte de pouvoir y lier n'importe quelle fonction de n'importe quelle classe? Ou bien il faut absolument qu'elles aient un ancêtre en commun?

troisiemetype:
Donc ce serait possible, dans le cas de ma classe d’encodeur rotatif, de faire en sorte de pouvoir y lier n’importe quelle fonction de n’importe quelle classe? Ou bien il faut absolument qu’elles aient un ancêtre en commun?

Même si cela ne relève pas d’une “bonne pratique” de la conception orientée objets, la réponse est oui, et la solution est en effet celle d’avoir un ancêtre commun. Mais en C++ tu peux hériter de plusieurs ancêtres à la fois donc cela ne pose aucun problème.
Ceci étant dit, même si ce n’est pas une obligation, pour une question de lisibilité et compréhension du code, il est conseillé (mais uniquement conseillé) de n’hériter que d’une seule classe “normale” (instantiable) et d’hériter d’autant de classes virtuelles pures (interfaces) que nécessaire.

Hello 3eType,

Pour résoudre ton problème il "suffit" d'appliquer le principe des classes à toutes les entités manipulées...

Tu t'es créé une classe SoundObject qui déclare toutes les méthodes dont tu as besoin pour faire du bruit.
Tu l'as héritée en autant de classes fille qu'il y a de types de sons...(Pad, StepSequencer, Bound, ...)

Fais la même chose pour ton encodeur...
Crée une classe EncodeurBase qui déclare toutes les méthodes propres à l'encodeur.
Ensuite, tu hérites de cette classe pour chaque type de comportement à supporter.

Dans ton code principal, tu déclares une variables currentSound de type SoundObject et une variable currentEncoder de type EncodeurBase.

En fonction de tes besoins, tu instancies un Pad ou un StepSequencer que tu assignes à currentSound, et l'une ou l'autre instance de classes héritée d'EncodeurBase que tu assignes à currentEncoder.

Ainsi, dans ta boucle principale, tu va manipuler un objet particulier sous sa forme "générique" ce qui va sérieusement te simplifier la vie.
Pour changer d'encodeur, il suffit de libérer l'instance actuelle, et d'en créer une nouvelle en l'assignant à currentEncoder.

Cela à l'air un poil compliqué mais en faits cela ne l'est pas si tu t'appliques un tout petit peu la première fois.

Bon amusement !

Coyotte

Zorro_X:
J'ai déjà tenté de t'expliquer comment éviter d'avoir besoin d'un pointeur sur fonction en C++ ici. Utiliser une classe d'interface t'évitera toujours d'avoir à recourir aux mécanismes et subterfuges des pointeurs sur fonction, qui est une fonctionnalité héritée du C, - presque - pas nécessaire en C++, sauf pour s'interfacer parfois avec du C...

J'ai écrit un petit sketch en m'appuyant sur ce que tu m'avais conseillé et cela donne les résultats escomptés. Peux-tu me confirmer que ce que j'ai écrit va dans le sens de ce que tu m'as conseillé de faire.

Merci d'avance.

Pierre

class interfaceBouton { // Classe interface
  public:
    virtual void surAppui(byte) = 0;
};

class Comp { // Classe d'où sont issus tous mes composants

};

class Bouton: public Comp {
    interfaceBouton *actionSurAppui;
  public:
    byte tag; // Ce qui va différencier les actions de chaque bouton
    Bouton(interfaceBouton *action): actionSurAppui(action) { // Constructeur

    }
    virtual bool select(int x, int y) {
      if (actionSurAppui != NULL) {
        actionSurAppui->surAppui(tag);
      }
    }
};

class Page: public Comp, public interfaceBouton {
  public:
    Bouton btn1;
    Bouton btn2;
    Page(): btn1(this), btn2(this) { // Constructeur
      btn1.tag = 1;
      btn2.tag = 2;
    }
    void surAppui(byte tg) {
      switch (tg) {
        case 1:
          Serial.println("Bouton 1 appuyé");
          break;
        case 2:
          Serial.println("Bouton 2 appuyé");
          break;
      }
    }
};


void setup() {
  Serial.begin(9600);
  Page maPage;
  maPage.btn1.select(5, 3); // Test si le bouton 1 a été appuyé
  maPage.btn2.select(25, 3); // Test si le bouton 2 a été appuyé
}

void loop() {
  // put your main code here, to run repeatedly:

}

oui, tout dépend de ce que tu veux faire dans ta fonction "surAppui()" mais en effet, t'as bien mis en œuvre la notion d'interface avec une classe virtuelle pure.

Zorro_X:
oui, tout dépend de ce que tu veux faire dans ta fonction "surAppui()" mais en effet, t'as bien mis en œuvre la notion d'interface avec une classe virtuelle pure.

Je te remercie beaucoup. Je vais essayer d'intégrer tout ça de manière à ce que cela devienne pour moi naturel.

Cordialement.

Pierre

Merci Coyote pour ces précisions!
En fait j'ai fait la moitié du chemin, puisque la classe SoundModule que j'ai créé, et dont héritent SoundPad, SoundStep et les autres à venir, est une classe virtuelle pure, justement pour n'avoir à gérer dans ma boucle principale que les fonctions de mise à jour, qui sont des methodes virtuelles pures. Ainsi chaque classe fille implémente la sienne, vide si besoin.

Donc, si j'ai bien compris, plutôt que de créer une classe Encoder qui possède une fonction exec() qui va exécuter une fonction attachée via un pointeur (ce qui est le cas pour l'instant), je crée une classe EncoderBase qui définit des méthodes pour le fonctionnement de base (en gros, exactement ma classe actuelle), plus une méthode virtuelle exec(), que je réimplémente dans des classes filles spécialisées pour l'un ou l'autre de mes besoins?
Mais comment ça se passe à ce moment-là pour lier une classe fille de EncoderBase à une classe fille de SoundModule? Est-ce qu'il faut que je fasse hériter ma classe SoundPad, par exemple de SoundModule ET de EncoderBase? Ou l'inverse, EncoderType1 doit hériter de SoundModule, ou de SoundPad?

Je vois un peu le principe de l'héritage, je vois à peu près à quoi correspondent une méthode virtuelle et une méthode virtuelle pure, mais c'est encore trop frais pour moi, je manque encore de l'expérience qu'amène la pratique pour en bien saisir tous les mécanismes, comment l'appliquer, et la portée que cela peut avoir sur la clarté et la souplesse...

Hello 3eType,

Tu as bien compris le principe.
Mais il n'est pas nécessaire d'hériter de SoundBase et de EncoderBase.... Il suffit d'instancier les bonnes classes.

Tu peux, par exemple prévoir un champ de type EncoderBase dans ta SoundModule.
Ainsi, une instance de SoundPad, SoundStep, ... peut se voir assigner une instance d'un enfant de EncoderBase et le tour est joué.

Pour expliquer les choses autrement, Imagine que tu aies une classe de base Maison avec une propriété MaPorte de Type Porte

Tu peux hériter la classe Maison en Chalet, BelEtage, Manoir.
De même ta classe Porte est héritée en PorteEnBois, PorteEnAcier, PorteEnVerre.

Une maison, c'est par exemple un chalet avec une porte en bois.
Tu instancies une classe Chalet et tu assigne à MaPorte une instance de PorteEnBois.

Ce peut être aussi un Chalet avec une porte en verre.
Tu instancies une classe Chalet et tu assigne à MaPorte une instance de PorteEnVerre.

J'espère que ce petit exemple aura été plus clair... :slight_smile:

Coyotte

Bonjour,

+1
Effectivement, une erreur de débutant est de vouloir utiliser l'héritage multiple pour hériter de tout et n'importe quoi.

Quand j'apprenais le C++ (ce qui commence à remonter à pas mal de temps), on nous a appris à nous poser la question 'est ce que mon objet est une sorte de ...'

Dans ton exemple
Est ce que le chalet est une sorte de maison -> oui. Donc on peut dériver chalet de maison
Est ce qu'une porte est une sorte de maison -> visiblement non. Par contre une maison contient une porte. Donc on peut inclure un objet porte dans maison.

Ok, c'est plus clair comme ça. En fait, ce qui m'a amené à poser la question de savoir s'il fallait faire hériter Pad de EncoderBase (ou faire hériter Chalet de Porte), c'est ça: si je rattache à une classe héritée de SoundModule une instance de EncoderBase, je deviens obligé d'effectivement implémenter un encodeur, non?

Ce que je voulais faire ici, c'était avoir quelque chose de réellement modulaire. Le programme principal crée une instance d'un encodeur, parce que l'interface physique en contient un. Le programme principal implémente dynamiquement des instances de SoundModule, en fonction des appels du menu principal.
Mais je voudrais que le système ne soit pas limité à un encodeur, et que si l'utilisateur qui va utiliser les classes SoundModule veut implémenter plutôt deux boutons poussoir pour faire ce que fait actuellement l'encodeur, il ait simplement à "rattacher" les fonctions voulues de SoundModule aux contrôles (boutons pousoirs, encodeurs, potentiomètres, etc.).
C'est pour ça que les pointeurs sur fonction me sont apparues comme étant une solution simple et efficace dans ce cas-là. Avec la limite de ne pouvoir accéder aux méthodes de classes. J'ai compris, sur un autre fil, pourquoi ce mécanisme était effectivement bloqué, et pourquoi c'était une bonne chose.

N'hésitez pas à me dire si je suis vraiment à la ramasse! Encore merci pour vos réponses éclairées et vos exemples!

Hello 3eType,

si je rattache à une classe héritée de SoundModule une instance de EncoderBase, je deviens obligé d'effectivement implémenter un encodeur, non

Oui... En général ce que l'on fait c'est que l'on crée l'encodeur de base, qui peut même être automatiquement instancié par un SoundModule.
Ainsi, pour un utilisateur qui ne souhaite pas se compliquer la vie,n tout est opérationnel.
Maintenant, le gars qui veut un encodeur particulier, lui, à la possibilité d'en créer un à sa main et de la faire fonctionner comme il l'entend.

Petit point de vocabulaire: Tu ne peux pas rattacher à une classe une instance, comme tu le dis ci-dessus.
Une classe, c'est le "patron", le "plan" de l'objet.

Pour reprendre l'exemple de mon post précédent, tu ne peux pas attacher une Porte au plan du chalet. (*)
(en tout cas, ce ne sera pas pratique :wink: )
Soit tu manipules des plans soit des objets mais pas les deux en même temps.

La programmation objet, ce n'est pas compliqué si tu maîtrises le vocabulaire et les concepts de base.

Avec un peu d'entraînement, ce sont des concepts qui vont devenir familiers.

Bon amusement !

Coyotte

(*) Oui, je sais... Il existe des situation extêmes ou une classe peut être manipulée elle-même en tant qu'objet mais on sort largement du cadre de ce sujet.