Trier des données sur des membres de classes

Merci à toi.

Oh, il s'est passé plein de choses ici depuis la dernière fois que je suis venu !

Merci beaucoup J-M-L pour les explications, et pour votre exemple concret sur l'héritage et son application. En cherchant à implémenter mon système j'avais effectivement trouvé (ou retrouvé, je l'avais lu mais ça faisait plusieurs mois que je n'avais pas codé, ça s'était un peu évaporé tout ça !) qu'en déclarant une méthode virtuelle dans la classe mère, la méthode de la classe fille serait appelée si disponible. C'est effectivement ce que j'ai mis en place, mais je suis tombé sur un autre problème, celui d'appeler une méthode sur la classe fille, qui n'existe pas dans la classe mère.
Pour s'en faire une idée :

class MenuItem{
  virtual const char* getName();
}

class MenuList : public MenuItem{
  const char* getName();
  void sort(const char* (*fn)()){
    // fonction de tri appelant la méthode get correspondante sur la classe générique
  }
}

class MenuSong : public MenuItem{
  const char* getName();
  const char* getArtist();
  const char* getAlbum();
}

Comme vous l'avez expliqué, décrire la méthode getName() comme virtuelle permet effectivement de passer à la fonction sort() n'importe quelle instance d'une classe dérivée de MenuItem, qui appellera la méthode de base ou la méthode surchargée, le cas échéant.
Par contre, si j'appelle sort(getArtist) sur une instance de la classe MenuSong, j'ai une erreur de compilation, puisque la classe MenuItem ne connaît pas cette méthode. Sur le principe c'est pourtant ce dont j'aurais besoin, puisque je sais quand j'appelle ma fonction de tri sur quoi je l'appelle, en l'occurrence une instance de MenuSong.
La difficulté est ici que je veux faire le tri sur une instance de MenuList (et sur ses enfants), donc il faut garder une méthode générale qui fonctionne sur la classe de base MenuItem, mais qui pourrait appeler des méthodes ajoutées sur les classes dérivées.

Ca n'est pas très grave, j'ai réussi à contourner efficacement le problème (et je crois de manière relativement propre) en définissant une fonction de tri externe qui récupère les pointeurs vers les deux objets à comparer, réalise le transtypage de la classe mère vers la classe fille (ou de l'objet général vers l'objet spécialisé, comme vous voulez) puis va chercher la méthode nécessaire.

Biggil, au sujet de la manière dont j'ai choisi d'organiser ce système de menu.
A mon sens c'est la manière la plus évidente, mais bien sûr c'est une question d'habitude et de besoin.
En fait dans une certaine mesure, j'ai appliqué l'idée de Linux, ou en terme de gestion tout est un fichier. Ou y est assimilé. Un fichier est un fichier (bien sûr !), un dossier est un fichier, un paramètre est un fichier. Bien sûr on accède pas de la même manière à une clef USB qu'à la luminosité de l'écran, mais les deux peuvent être écris et lus de la même manière, ce qui en fait un système souple.

J-M-L a très bien expliqué l'idée sous-jacente : pour moi un menu, ce sont des items. Certains items appellent une fonction, d'autres items sont eux-même des (sous-) menus.

Il me paraissait donc logique de décrire une classe de base, MenuItem, qui groupe ce qui caractérise une entrée de menu :

  • un nom, qui sera affiché lorsque le menu est appelé,
  • un parent : à quoi est rattaché l'item,
  • une fonction : que se passe-t-il si on sélectionne cet item.

Un menu en-lui même est effectivement une liste, on peut en faire une classe différente, mais comment avoir des menus imbriqués ? Pour moi faire de MenuList une spécialisation de MenuItem permet de régler le problème : la gestion concrète lors de la création d'un menu est exactement la même. On ajoute de la même manière un nœud terminal ou un sous-menu. Les méthode appelées sont les mêmes aussi. L'unique différence est que le menu général n'a pas de parent. Il faut donc faire en sorte de ne pas pouvoir appeler le parent du menu, ce que j'ai réglé de la manière suivante : si l'item n'a pas de parent, il renvoie un pointeur vers lui-même.

A partir de là la génération peut être automatisée, partiellement ou totalement, et la navigation dans le menu se fait avec les mêmes fonctions dans le programme principal.
Pour vous donner une idée de ce que ça donne en pratique dans le cas de mon projet de baladeur :

playing // afficher le lecteur, avec la lecture en coursmusic
  artists
    albums
      [...]
        [...]
  albums
    [...]
  songs
    [...]settings
  volume max
  mentions légales

Le menu de base est créé "à la main", ainsi que le sous-menu "settings".
Les menus liés à la musique sont générés dynamiquement à partir d'un fichier XML, lui-même généré et mis à jour (ajouts et retraits) à partir du contenu de la carte SD.
A chaque entrée de menu est associée une fonction qui définit quoi faire lorsque l'on valide le menu. Lorsque c'est une liste, la fonction permet de descendre d'une niveau. Lorsque c'est un nœud terminal c'est accéder au réglage, lancer le morceau, etc.
Cela permet une certaine souplesse (par exemple selon le type de fichier audio je ne vais pas appeler la même fonction), et cela permet aussi de ne pas avoir à gérer chaque cas spécifique pour la navigation : chaque bouton est lié à une fonction (rentrer ou sortir d'un menu, naviguer en bas ou en haut). Ainsi le bouton valider appelle toujours la même fonction, quel que soit le type d'item spécifié. C'est lors de la création du menu (manuelle ou dynamique) que la fonction est rattachée à l'item.

Il y a sûrement d'autres manières d'implémenter un menu, j'en ai effectivement trouvé d'autres en lisant le code de projets qui contiennent une interface graphique, mais dans mon cas il me semblait qu'ils entraîneraient une foule de cas particuliers

La bibliothèque va encore évoluer (notamment pour faire du nettoyage, enlever certaines méthodes qui n'ont plus lieux d'être, commenter et documenter), mais elle est hébergée sur github, si ça vous intéresse d'en savoir plus.

Pour finir sur l'organisation de ce menu, quelques notes.
Je me rends compte que l'idée de dériver la classe MenuItem pour la spécialiser est peut-être moins pertinent que d'en faire un conteneur vide contenant des données.
J'ai donc commencé à explorer l'idée de faire de MenuItem un template de classe. Ca fonctionne bien jusqu'au moment où je crée des instances MenuList; les méthodes qui prennent en paramètre ou retournent un MenuItem posent problème.
L'autre solution est de rattacher à chaque MenuItem un pointeur et une taille, afin de lui attribuer des données arbitraires, par exemple une structure définissant un morceau.

class MenuItem{
  void *_data;
  uint32_t _dataSize;
}

L'idée semble séduisante et souple. L'inconvénient c'est qu'il faut savoir ce que l'on manipule comme données, et qu'il n'y a plus de garde-fou dans le code.

A suivre...

vous pouvez aussi laisser tomber l'approche objet et créer un arbre de struct qui contiendraient des éléments communs (le nom, un descripteur de type) et une union dont le contenu est variable en fonction du descripteur - ce qui vous permet de représenter différents truc sous le même "type"

struct t_group1 {
...
}; 

struct t_group2 {
...
}; 

struct t_group3 {
...
}; 

union t_groupDeTrucs {
 t_group1 dataType1;
 t_group2 dataType2;
 t_group3 dataType3;
};

struct t_item {
  const char* nom;
  const uint8_t descripteur;
  funcPtr callback;
  t_groupDeTrucs data;
}

troisiemetype:
Par contre, si j’appelle sort(getArtist) sur une instance de la classe MenuSong, j’ai une erreur de compilation, puisque la classe MenuItem ne connaît pas cette méthode.

Et voilà. Nous y sommes.

Comme je le dis depuis le début, tu utilises l’héritage de la mauvaise façon. Plus loin tu justifies ta façon de faire, mais je ne veux pas rentrer dans les détails (ce n’est pas mon projet).

Si la classe B hérite de la classe A, ça veut dire que tout objet de type B est aussi, complètement, un objet de type A.

C’est la règle de l’héritage en C++.
Si tu ne la respectes pas, tu vas avoir des ennuis (et visiblement ça a déjà commencé : ton organisation en classes est mal foutue).

Oui sur le fond, il faut respecter un modèle objet (mais sans refaire les débat une étiquette active comme classe mère avec comme classe fille une étiquette active étendue par un groupe d’étiquettes actives respecte la définition de “is a”).

C’est plus la promesse que vous avez fait au compilateur du type de la variable que vous ne respectez pas, plus que les principes objets (ou une sous classe a le droit d’étendre la classe mère en comportement (méthodes) ou attributs (variables)).

Le problème vient du typage de certaines variables contenant une instance de la sous classe avec le type de la classe mère et ensuite d’essayer d’appeler une méthode qui n’existe que dans la sous classe qui vous oblige à jongler avec des cast - à ce niveau là autant prendre l’approche pure C avec structure et unions et le cast correspond à l’usage du bon membre de l’union.

C’est la discussion que nous avions précédemment - quelle que soit l’approche il faudra que le conteneur contienne des instances typées à une classe mère commune qui a mon avis se doit d’être pure virtuelle avec TOUTES les méthodes définies au niveau de la classe mère ce qui est l’équivalant de protocole dans d’autres langages de programmation- on indique que l’instance doit respecter une API

Pour éventuellement rendre les choses plus dynamiques il faudra sans doute une méthode d’introspection si vous avez besoin de tester le type plus précisément.

Merci J-M-L pour l'idée des structs + unions. C'est une approche que je n'avais pas envisagé, même si comme je le disais l'idée de faire de MenuItem un simple conteneur de données arbitraires est une possibilité. Dans ce cas-là MenuSong disparaîtrait au profit d'une structure décrivant le morceau (album, artiste, chemin sur le disque, etc.), rattachée à un MenuItem.

Biggil, je comprends fort bien que mon approche ne corresponde pas à ce que vous avez ou auriez mis en place, mais je ne vois pas dans quelle mesure j'utilise l'héritage d'une mauvaise façon. Je suis d'accord avec vous, un menu est un conteneur. On peut l'envisager comme une entité indépendante, à laquelle on rajoute des enfants, éléments terminaux ou sous-menus. Pourtant, si l'on considère qu'un sous-menu a probablement les mêmes besoin qu'un menu principal (ajouter ou retirer des enfants, faire naviguer le focus d'un enfant à un autre, etc.), alors je ne vois pas de raison de faire une différence entre le menu principal et un sous menu. La seule différence pratique est qu'un menu principal n'a pas, bien sûr, de parent.
Mais une liste a toutes les caractéristiques d'un item : elle peut avoir le focus, elle peut être activée (cliquée), elle peut avoir un nom. Et comme elle est une liste et qu'elle contient des éléments, on peut aussi lui ajouter ou lui retirer des enfants, trier ses enfants, etc.
De la même manière, dans mon cas, une chanson, puisqu'elle apparaît dans le menu, peut être un item. Et comme c'est un item d'un type particulier, je rajoute aux caractéristiques de base d'un item (nom, fonction, parent) d'autres caractéristiques spécifiques (nom d'artiste, nom d'album, numéro de piste, etc.)
Pour moi c'est l'idée même de l'héritage : une classe mère définit des traits communs, les classe filles modifient le comportement de ces traites communs (méthodes surchargées ou virtuelles) ou en ajoutent.
Un carton de bouteilles n'est pas une bouteille, mais tous les deux sont des contenants.

Pour revenir à votre première intervention, une liste est un item, mais bien sûr pas de cette même liste. C'est un item de la liste parente, ou sans parent s'il s'agit du premier niveau.

MenuList // Menu global, pas de parent
  MenuItem // nœud terminal, fonction attachée
  MenuList // sous-menu
    MenuList // sous-sous-menu
      MenuItem
      MenuItem
      MenuItem
    MenuItem
    MenuItem
    MenuList
      MenuItem