Trier des données sur des membres de classes

Bonjour à tous !

Pour placer le contexte : je suis en train de réaliser un lecteur audio portable, l’idée est de faire plus ou moins l’équivalent d’un ipod.

J’ai passé ces derniers jours à travailler sur des classes pour gérer les menus. C’est organisé de cette manière :
Il y a une classe de base MenuItem qui correspond à une entrée de menu, qui permet de gérer le minimum vital :

  • un nom (pour l’affichage du menu)
  • une fonction callback attachée pour quand on “rentre” dans cette entrée de menu.

Il y a une deuxième classe MenuList qui hérite de celle-ci, qui permet de définir une liste :

  • ajouter / retirer / supprimer des enfants (qui peuvent être la classe de base ou une autre liste)
  • obtenir le premier / précédent / suivant / dernier élément
  • gérer des index pour garder la trace de l’entrée active (selectionnée à l’écran)
  • tester si un enfant donné fait partie de la liste
  • trier la liste par nom des enfants.
  • etc.

Cela donne un système relativement souple qui permet de générer très rapidement des menus conséquents, par exemple à partir d’un fichier XML.

Maintenant, ce qui m’amène : j’ai prévu de créer une classe pour les morceaux, qui hérite de la classe de base, pour ajouter les champs de texte suivants :

  • numéro de piste
  • nom d’artiste
  • nom d’album
  • année de sortie
  • etc.

Faire le tri sur un champ donné n’est pas très compliqué avec une fonction dédiée, c’est ce que j’ai fait plus haut. La fonction de tri va chercher le nom des deux enfants, fait la comparaison, et réorganise si nécessaire :

void MenuList::sort(){
// _children est un tableau de pointeurs vers les enfants rattachés à la liste
  uint16_t limit = _childrenSize - 1;
  for(uint16_t i = 0; i < limit; ++i){
    int16_t result = strcmp(_children[i]->name(), _children[i + 1]->name());
      if(result > 0){
       swap(i);
      }
    }
  }
}

// depuis le programme :
menu->sort();

(note : j’ai raccourci la fonction, qui bien sûr remonte lorsqu’elle inverse deux valeurs)

Ce que je voudrais pouvoir faire c’est appeler la fonction de tri en lui passant en argument la méthode qui retourne le champ désiré de la classe enfant. Quelque chose comme :

void MenuList::sort(const char* (*_fn)(void)){
  uint16_t limit = _childrenSize - 1;
  for(uint16_t i = 0; i < limit; ++i){
    int16_t result = strcmp(_children[i]->fn(), _children[i + 1]->fn());
      if(result > 0){
       swap(i);
      }
    }
  }
}

// depuis le programme :
menu->sort(MenuItem::name);
// ou bien
menu->sort(MenuItem::artist);

Mais je sèche. Passer un pointeur de fonction à une méthode, je sais faire, mais passer un prototype pour que la méthode l’adapte à l’instance qu’elle est en train de manipuler, je ne comprends pas comment le réaliser.

Je suis à peu près certain que l’on doit pouvoir faire ça, mais comment ? Si vous savez, merci par avance !

Ok, comme souvent la réponse vient quand on pose la question. Formuler un problème à l'écrit permet de le formaliser.

Il apparaît qu'il suffisait de trouver le bon terme de recherche pour trouver la bonne réponse. Cela porte un nom, c'est pointeur vers methode (pointer to method), assez similaire à un pointeur vers fonction (pointer to function).

La principale difficulté est la formalisation dans le code. Cela donne donc ça, que je viens de tester, qui compile et fait ce qui est prévu.
menu.h :

class MenuItem{
  const char* getName();
}

class MenuList : public MenuItem{
  void sort(const char* (MenuItem::*fn)());
}

C'est assez similaire au passage d'un pointeur de fonction : la syntaxe doit mentionner le type retournée (ici const char*) et les arguments (ici aucun). Notez où apparaît le pointeur !

menu.cpp

const char* MenuItem::getName(){
  return _name;
}

void MenuList::sort(const char* (MenuItem::*fn)()){
  uint16_t index = 0;
  (_children[index]->*fn)();
}

Notez la syntaxe spécifique pour l'appel de la méthode pointée, sur le membre concerné ! Le pointeur doit être précisé devant la méthode, comme dans le cas d'une fonction simple. Mais tout le groupe (objet + méthode pointée) doit être placé entre parenthèses.
Dans le cas où l'on veut appeler la méthode sur un objet plutôt que sur un pointeur vers un objet, la syntaxe est équivalente :

(_children[index].*fn)();

Et enfin, l'appel de la fonction dans le programme principal :

main{
  list->sort(&MenuItem::getName);
}

Note : il m'a fallu un moment pour parvenir à le faire fonctionner. J'avais deux fonctions name() :

// setter
void name(const char* name);
// getter
const char * name();

Le compilateur n'était pas capable de trouver la bonne fonction, et cherchait à transtyper. Visiblement les pointeurs vers méthodes n'aiment pas les fonctions surchargées. J'ai donc remplacé les deux signatures par deux fonctions différentes pour supprimer le problème.

Je suis bien content d'avoir compris ça aujourd'hui, j'espère que cela pourra servir à quelqu'un d'autre !

Update !

Ce que j’ai décrit plus haut marchait très bien.
Seul problème, quand je dérive la classe de base MenuItem pour lui rajouter des données et que je veux attacher une fonction qui sera utilisée par sort, le compilateur refuse, puisque la fonction que je lui passe ne fait pas partie de la calsse de base. C’est à s’arracher les cheveux.

J’ai fini par prendre le problème dans l’autre sens, à savoir que je définis dans le programme les fonctions de tri dont j’ai besoin, et je passe à la fonction de tri la référence à ces fonctions, au besoin. Ca donne à peu près ça :

Menu.h :

class MenuList{
  void sort(void (*fn)(MenuItem*, MenuItem*));
};

Menu.cpp :

void MenuList::sort(void (*fn)(MenuItem*, MenuItem*)){
  // ...
  for(uint8_t i = 0; i < _childrenSize; ++i){
    int16_t result = fn(_children[i], _children[i + 1]);
    // ...
  }
}

Et dans mon code, je définis une fonction qui prend deux pointeurs vers deux éléments, qui fait le tri et renvoit une valeur nulle, positive ou négative.
main.cpp :

int16_t sortByText(MenuItem *item1, MenuItem *item2){
  return strcmp(item1.name(), item2.name());
}

MenuList list = MenuList();
// Disons qu'elle a déjà plein de MenuItem rattachés !
list.sort(sortByText);

En fait c’était tellement simple de le faire dans ce sens-là… L’inconvénient c’est que si l’on veut accéder à des fonctions des objets héritant de la classe de base, il faut quand même forcer le transtypage dans la fonction de tri.

Mais ça fonctionne parfaitement, je peux maintenant générer en un clin d’œil des menus triés par artiste, albums, chanson, numéro de piste, année, etc. :slight_smile:

Seul problème, quand je dérive la classe de base MenuItem pour lui rajouter des données et que je veux attacher une fonction qui sera utilisée par sort, le compilateur refuse, puisque la fonction que je lui passe ne fait pas partie de la calsse de base. C’est à s’arracher les cheveux

==> essayez en déclarant vos méthodes comme virtual,

Voici un exemple que j’avais écrit pour démontrer le polymorphisme

J’ai une classe Person qui a deux sous classes, VIP et Arduinist. J’instancie des VIPs et des Arduinists que j’ajoute dans un tableau de type Person.

Ce faisant comme vous le dites vous avez typé les pointeurs mais comme la fonction appelée a été définie comme étant virtuelle, ça permet le polymorphisme (interface de programmation unique à des entités ayant différents types rééls) et le compilateur fera un late binding au lieu d’un early binding (en gros il va trouver la méthode à appeler à l’exécution en fonction de la classe réelle de l’objet pointé)

const uint8_t maxStringSize = 30;
// -----------------------------------------------------------------------------------------
class Person {
  protected:
    bool isMale = true;
    char personName[maxStringSize + 1]; // +1 for trailing null char

  public:
    Person(const bool m, const char* n): isMale(m)
    {
      if (n) strncpy(personName, n, maxStringSize);
      else strncpy(personName, "no name", maxStringSize);
      personName[maxStringSize] = '\0'; // just in case
    }

    virtual float information()   // without virtual you don't get polymorphisme -> early binding. otherwise late binding
    {
      Serial.print(F("[member=752349]person[/member]\tname: "));
      Serial.print(personName);
      Serial.print(F("\tsex: "));
      Serial.print(isMale ? F("male") : F("female"));
      return 0;
    }
};
// -----------------------------------------------------------------------------------------
class VIP: public Person {
  protected:
    uint16_t keyYear;
    char famousFor[maxStringSize + 1]; // +1 for trailing null char

  public:
    VIP(const bool m, const char* n, uint16_t y, const char* majorTopic): Person(m, n), keyYear(y) {
      if (majorTopic) {
        strncpy(famousFor, majorTopic, maxStringSize);
        famousFor[maxStringSize] = '\0'; // just in case
      }
    }

    virtual float information() override  // without virtual you don't get polymorphisme -> early binding. otherwise late binding
    {
      Person::information();
      Serial.print(F("\n\t@VIP:\tFamous for: "));
      Serial.print(famousFor);
      Serial.print(F("\tdone from year: "));
      Serial.print(keyYear);
      return 0;
    }
};
// -----------------------------------------------------------------------------------------
class Arduinist: public Person {
  protected:
    uint16_t joinedForumInYear;

  public:
    Arduinist(const bool m, const char* n, uint16_t y): Person(m, n), joinedForumInYear(y) {}

    virtual float information()  override // without virtual you don't get polymorphisme -> early binding. otherwise late binding
    {
      Person::information();
      Serial.print(F("\n\t@Arduinist:\tJoined forum in: "));
      Serial.print(joinedForumInYear);
      return 0;
    }
};

// -----------------------------------------------------------------------------------------

VIP lovelace(false, "Ada Byron (Lovelace)", 1842, "abstract science of operations");
VIP ritchie(true, "Dennis Ritchie", 1972, "the C language");
VIP stroustrup(true, "Bjarne Stroustrup", 1983, "the C++ language");

Arduinist jeanmarc(true, "J-M-L", 2015);
Arduinist lesept(true, "lesept", 2017);
Arduinist troisiemetype(true, "troisiemetype", 2016);

Person* team[] = {&lovelace, &ritchie, &stroustrup, &jeanmarc, &lesept, &troisiemetype};
uint8_t teamSize = sizeof(team) / sizeof(team[0]);

float (Person::*fonctionToCall)() = &Person::information; // example with pointer to member function

void setup()
{
  Serial.begin(115200);
  for (byte i = 0; i < teamSize; i++)  {
    (team[i]->*fonctionToCall)(); // similar to calling team[i]->information(); but using the pointer (which could be dynamically chosen)
    Serial.println('\n');
  }
}

void loop() {}

J’ai rajouté la déclaration d’un pointeur sur fonction membre fonctionToCall en faisantfloat (Person::*fonctionToCall)() = &Person::information;et c’est ce que j’utilise à la fin du setup() pour parcourir mon tableau de personnes et on voit bien que ce sont les méthodes de chaque sous classe qui sont bien appelées
le moniteur série (@ 115200 bauds) affichera

[color=purple]
[member=752349]person[/member]	name: Ada Byron (Lovelace)	sex: female
	[color=red]@VIP[/color]:	Famous for: abstract science of operations	done from year: 1842

[member=752349]person[/member]	name: Dennis Ritchie	sex: male
	@VIP:	Famous for: the C language	done from year: 1972

[member=752349]person[/member]	name: Bjarne Stroustrup	sex: male
	@VIP:	Famous for: the C++ language	done from year: 1983

[member=752349]person[/member]	name: J-M-L	sex: male
	[color=red]@Arduinist[/color]:	Joined forum in: 2015

[member=752349]person[/member]	name: lesept	sex: male
	@Arduinist:	Joined forum in: 2017

[member=752349]person[/member]	name: troisiemetype	sex: male
	@Arduinist:	Joined forum in: 2016


[/color]

troisiemetype:
Il y a une classe de base MenuItem qui correspond à une entrée de menu...
Il y a une deuxième classe MenuList qui hérite de celle-ci, qui permet de définir une liste ...

Ouh là, cette conception me paraît louche !
Si le classe MenuList hérite de la classe MenuItem, c'est qu'un objet MenusList est un (is a) MenuItem.
On ne tergiverse pas avec ça, c'est la définition de l'héritage en C++.
Comme une liste pourrait-elle être aussi un item de ... cette liste ?

biggil:
Ouh là, cette conception me paraît louche !

Non ce n'est pas louche

La MenuList dispose d'un nom et d'une fonction de callback sans doute donc c'est bien un Item (qui va apparaître dans un menu et le callback faire se déplier les sous menus), et cette classe va avoir une variable d'instance supplémentaire qui sera j'imagine une liste de pointeurs vers des MenuList pour obtenir une récursivité (menu, sous menu, sous sous menu, ....)

(deleted)

je ne comprends pas la question.

Dans l'absolu Un pointeur sur fonction n'est que l'adresse du bout de code à exécuter. Ce pointeur a sa vie propre en dehors ou au sein de la classe.

Les fonctions virtuelles sont le cœur même de la programmation orientée objet puisqu'elles permettent le polymorphisme. Dans ce cas lorsque l'on prend l'adresse d'une fonction membre virtuelle comme je l'ai fait avec float (Person::*fonctionToCall)() = &Person::information; // example with pointer to member functionalors, à l'usage de ce pointeur, le compilateur génère un code spécifique pour retrouver la fonction en fonction du type de l'instance (late binding).

En interne, cela est accompli en créant une table virtuelle (VTABLE) pour chaque classe qui contient une fonction virtuelle. Le compilateur place l'adresse de la fonction virtuelle dans la VTABLE spécifique de cette classe et une variable de classe supplémentaire est crée, elle contient un pointeur tacite (appelé pointeur virtuel VPTR) qui pointe directement vers la VTABLE de cette classe.

Lorsque nous appelons la fonction virtuelle via le pointeur de classe de base, le compilateur ne fait pas un appel en langage assembleur vers une adresse particulière, mais génère un code différent qui utilise VPTR et va rechercher dans la VTABLE liée à l'instance cible le bon pointeur vers la fonction à exécuter.

(deleted)

oui C’est comme cela qu’une partie magique (objet) du C++ fonctionne - sinon on serait très proche du C

Je ne vois pas trop votre point sur au sein ou en dehors.

Après compilation la notion de classe n’a plus de sens vraiment - vous avez du code exécutable et des structures de données sur lesquelles le code peut s’appliquer mais vous n’avez pas de run time / JIT.

J-M-L:
La MenuList dispose d'un nom et d'une fonction de callback sans doute donc c'est bien un Item

La MenuList représente le Menu lui-même : une liste d'item, qui eux ont chacun un label et un callback.
Je ne vois pas de raison pour laquelle cette liste aurait besoin pour elle d'un nom et d'un callback.

La MenuList possède les items (c'est une relation has a) [expression consacrée en POO]
Je ne vois aucune relation de type is a , caractéristique de la notion d'héritage.

Alors, oui, on peut de toutes façons implémenter ça comme on veut, mais aller à l'encontre d'une des bases du C++ s'avère souvent contre-productif.

Les dénominations item et menuList sont peut être mal choisies et induisent sans doute vos questions.

Je ne vois pas de raison pour laquelle cette liste aurait besoin pour elle d'un nom et d'un callback.

Quand vous regardez votre barre de menu vous voyez des étiquettes (Fichier, Édition,...) et quand vous clickez sur cette étiquette une action est déclenchée.

Quand l’étiquette est une “feuille” de l’arbre des menus l’action est appliquée sur l’application ou ses données (copier, coller, ouvrir un fichier, sauver le fichier,...) et si l’étiquette est une branche alors son action c’est d’afficher les branches et feuilles qui sont sous jacentes (sous menu).

Donc il y a bien d’un point de vue conceptuel pour tout ce qui est un “élément de menu” (feuille ou branche - un Item) la notion d’étiquette textuelle et d’une action à déclencher. Ce n’est donc pas aberrant de dire que ces éléments forment la classe de base et c’est suffisant pour une feuille de l’arbre des menus.

Ensuite ça n’est pas suffisant pour les branches qui doivent avoir une liste gérée de sous menus (qui sont des feuilles ou des branches) et donc il est naturel de dire que une branche c’est une paire étiquette/action augmentée d’une liste polymorphe de feuilles ou de branches. D’un point de vue affichage on peut dire aussi que c’est une spécialisation: si vous êtes un Item vous affichez juste votre texte, si vous êtes un menu vous affichez votre texte (super.desssiner()) donc la fonction de la classe parent, que vous surchargez/spécialisez par l’ajout d’une petit triangle qui montre que cette étiquette dispose d’un sous menu.

On est donc bien dans le cas d’une sous classe, on a spécialisé un concept plus général en y ajoutant des attributs (la liste) et en spécialisant son callback (ouverture du sous menu).

un autre exemple du besoin de label et de callback pour un élément pilotant une sous liste de menus ce sont les menus dynamiques, par exemple sur l'IDE arduino:

Je ne vois pas en quoi on casse le C++ ou la POO.

Quel modèle objet alternatif proposez vous pour représenter un menu ? (Avec la possibilité de mixer dans un affichage de menu des feuilles et des branches de manière uniforme. On pourrait avoir une liste d’objets respectant une certaine API - un protocole - vous appelez cela comme vous voulez suivant le langage et ses capacités).

Quel modèle objet alternatif proposez vous pour représenter un menu ?

Voici ce que j'utilise assez souvent:
la classe Menu ave ses méthodes Append ( item ) ou AppendSubMenu ( menu )
la classe MenuItem
Pour l'idée que je ne fais de la POO (dans ma tête à moi), l'héritage est la façon d'exprimer une relation is a.
Ca me paraît fondamental. J'ai pu constater de nombreuses fois que des dérivations impropres mettaient la panique dès de le programme se complexifie.

Oui je suis d'accord que l'héritage exprime "is a" mais ce qui est expliqué plus haut montre bien qu'on est bien dans le cas d'une spécialisation d'un "label actif" => un menu est un "label actif" qui dispose en plus d'une liste de "label actif" (menus ou juste label)

Dans votre approche

la classe Menu avec ses méthodes Append ( item ) ou AppendSubMenu ( menu )
la classe MenuItem

=> Quel est le type des éléments dans la liste du Menu ? (si ce sont des void * vous avez cassé le modèle objet, si c'est un pointeur vers une "racine commune" (en terme d'héritage) à item et menu, vous retombez dans notre cas).

On ne doit pas parler de la même chose. Pour moi un Menu est un boîte qui contient des Items. Les Items ont un comportement (label, callback). Le Menu est le conteneur, c'est tout.
Un carton de bouteilles de Beaujolais n'est pas une bouteille de beaujolais. Il a des bouteilles.

Si, si - on parle globalement de la même chose et je vois bien ce que vous voulez dire. Un carton n'est pas un type de bouteille :slight_smile: - mais ce n'est pas non plus le paradigme retenu. pour reprendre votre image, on parlerait d'étiquettes; vous en avez sur les bouteilles et sur les cartons, mais les étiquettes des cartons sont un peu spéciales, elles doivent décrire tout le contenu du carton. Ce n'est pas aberrant de dire qu'une étiquette de carton est un type spécial d'étiquette ?

Dans votre approche La question c'est comment vous stockez ce qui est dans le conteneur: quel est le type de la liste des éléments du conteneur ? ça ne peut pas être que des Items (des bouteilles de beaujolais) puisqu'il faut pouvoir supporter la notion de sous menu (d'autres cartons, récursivement)

Pour illustrer:
Dans cette image:


Fichier est un carton
Dans ce Menu "Nouveau" et "Ouvrir" sont des bouteille mais Exemples est un Carton qui contient plein de cartons dont certains ont des cartons et des bouteilles. (le carton 09.USB contient 2 cartons (Keyboard et Mouse) et une bouteille keybpardAndMouseControl)

Ma question: quelle représentation OO pratique adopter pour la liste (ordonnée) de ce que contient le carton pour avoir cette possibilité de récursivité ?

Je ne vois pas où est la difficulté ?
Un Menu contient une liste de d'Items. C'est tout.
Maintenant il y a deux sortes d'Items : des "Actions Directes" et des "Lancement de sous-menu" (2 classes dérivées d'un même classe de base, ou une seule classe). Ces 2 là ont des réflexes différents lorsqu'ils sont activés (ou survolés...)

Fichier est un carton

pas d'accord, "Fichier" est un item. Un label + un reflexe qui est d'ouvrir un Menu.

"Nouveau" et "Ouvrir" sont des bouteille mais Exemples est un Carton qui contien

pas d'accord, "Exemples" est un Item de type "lanceur de sous-menu". C'est une bouteille magique qui contient (has a) un carton d'autres bouteilles. Elle n'est pas ce carton, elle l'a.

Item = label + action associée (qui peut être d'ouvrir un sous menu, lui-même objet Menu)
Menu = conteneur d'items (+position x,y, autres...)

Pas de difficulté, je comprends ce que vous dites, ça se tient et se défend.

je cherchais à comprendre comment vous évitez la classe étiquette et une sous classe particulière pour les groupes ou 2 classes dérivées d'un même classe de base.

Dans ce que vous dites vous ne le faites pas et vous revenez à dire qu’une étiquette de carton est (is a) un type d’étiquettes, non ? Et cela est suffisant pour gérer des menus donc pourquoi s’encombrer de plus ?

En faisant cela

Item = label + action associée (qui peut être d'ouvrir un sous menu, lui-même objet Menu)
Menu = conteneur d'items (+position x,y, autres...)

vous ne dites pas qui porte la connaissance du conteneur. Soit vous le mémorisez dans l’item (ma notion d’étiquette de carton) soit vous déportez la connaissance dans le callback et vous perdez le modèle objet.

Ca devient trop compliqué, je ne capte pas tes notions d'étiquettes.
De toutes façons quand je lis "on attribue un callback au widget", c'est du C, pas de l'objet. J'ai travaillé des années avec le système graphique QNX/Photon, le générateur d'interface marchait comme ça (produire du code C), et ce n'était jamais évident de rentrer ça en force dans un monde d'objets.

En postant, je ne désirais pas entrer dans les détails de comment tout ça s'implémente en C++, s'il faut 2 classes Item ou une seule, etc. Je crois qu'on peut faire les 2, si tu as jeté un oeuil à la doc wxWidgets (post #12), tu vois qu'il n'y a qu'une seule classe wxMenuItem, très peu utile au final, dont les instances sont fabriquées par le Menu lui-même.. C'est bien le wxMenu qui est prévenu qu'une "case" est une action par menu.Append ( event_id, label ) ou bien un sous menu par menu.AddSubMenu ( menu2, label). Il y a peut-être une cuisine interne un peu crade, ou pas, mais ça reste conceptuellement simple et cohérent.

J'avoue avoir réagi sur cette architecture (Menu hérite de Item) sans avoir lu les détails du code proposé, et donc réagi selon ce que moi j'imagine être un Menu (une boîte graphique, x, y, etc) et un Item (un label et un callback, à choisir entre "action" et submenu").
Dans cette acceptation, le Menu ne peut pas dériver de l'Item (c'et quoi le label du Menu ? on le voit jamais ? et le call back ? y'en a pas besoin !)

Pardon pour la prise de tête :slight_smile:

pas de souci, la discussion est intéressante - je n'y vois pas une prise de tête. Merci pour l'échange