IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

SL & STL C++ Discussion :

Est-il possible d'étendre facilement std::map ?


Sujet :

SL & STL C++

  1. #1
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 461
    Par défaut Est-il possible d'étendre facilement std::map ?
    Bonjour à tous,

    La question est probablement récurrente mais je n'ai pas trouvé de fil traitant directement de ce cas (ou alors je ne suis pas descendu suffisamment profondément). Est-il possible d'écrire une classe dérivée qui descende de std::map et utilisant pour ses entrées une classe elle-même dérivée de std::pair ? L'idée étant bien sûr, comme toujours, de préserver au maximum le comportement habituel des objets usuels et le bénéfice de ce qui a été écrit pour nous. On considère en outre que l'on ne descend jamais en dessous de C++11.

    Le problème initial est probablement un « XY », comme on le dit : je refactore du code écrit par quelqu'un qui est très compétent dans son domaine mais a priori pas développeur, et qui l'était encore moins quand il a dû entamer l'application concernée (en reprenant certains cours suivis quelques années plus tôt). Entre autre choses, il parse souvent un fichier de configuration texte écrit sous la forme « CLÉ = VALEUR » en ré-extrayant à la main son contenu au début de tous les exécutables qu'il produit.

    L'idée, somme toute assez classique, a donc été d'écrire un objet « Config » faisant ce travail pour lui et se présentant directement comme une map, pour pouvoir utiliser value = config["key"].

    Tout cela fonctionne très bien jusqu'ici, mais l'idée était d'ajouter également des propriétés aux entrées elles-mêmes du mapping, spécialement lorsque l'on itère dessus avec for (auto entry : config), par exemple. Et en particulier, j'aurais bien aimé ajouter des alias « entry.key » et « entry.value » plutôt qu'être contraint de continuer à utiliser « entry.first » et « entry.second » dans ce cas précis. En l'occurrence, même si on aimerait à terme ajouter éventuellement des fonctions-membres, ces seuls alias pourraient être implémentés par références simples définies par le constructeur et qui ne modifieraient pas du tout l'empreinte en mémoire des instances de de ces objets.

    L'entrée consacrée de cppreference indique qu'il est possible d'indiquer Allocator dans les paramètres template, mais que value_type est définie « en dur », ce qui laisse penser, après quelques tests, que le prototype de la classe nous laisse choisir notre allocateur à condition que celui-ci continue d'utiliser normalement une std::pair, chose qui semble être confirmée par une static_assert() dans le header de G++ (v 14.2.1) :

    Code C++ : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
      template <typename _Key, typename _Tp, typename _Compare = std::less<_Key>,
                typename _Alloc = std::allocator<std::pair<const _Key, _Tp> > >
        class map
        {
        public:
          typedef _Key                                      key_type;
          typedef _Tp                                       mapped_type;
          typedef std::pair<const _Key, _Tp>                value_type;
          typedef _Compare                                  key_compare;
          typedef _Alloc                                    allocator_type;
     
        private:
    #ifdef _GLIBCXX_CONCEPT_CHECKS
          // concept requirements
          typedef typename _Alloc::value_type               _Alloc_value_type;
    # if __cplusplus < 201103L
          __glibcxx_class_requires(_Tp, _SGIAssignableConcept)
    # endif
          __glibcxx_class_requires4(_Compare, bool, _Key, _Key,
                                    _BinaryFunctionConcept)
          __glibcxx_class_requires2(value_type, _Alloc_value_type, _SameTypeConcept)
    #endif
     
    #if __cplusplus >= 201103L
    #if __cplusplus > 201703L || defined __STRICT_ANSI__
          static_assert(is_same<typename _Alloc::value_type, value_type>::value,
              "std::map must have the same value_type as its allocator");
    #endif
    #endif

    À la limite, je n'ai pas besoin de virtualisation ici, donc ce ne serait pas un problème de redéfinir le type public value_type au niveau de ma propre classe et faire en sorte que le template utilise le même mais comme l'assertion est statique, elle est intrinsèque à std::map et se déclenche avant d'atteindre la classe dérivée.

    Je comprends également que le LSP impose que la classe dérivée soit pleinement compatible avec sa classe-mère, ce qui l'oblige à renvoyer les mêmes objets lorsque l'on itère dessus car même s'ils sont effectivement dérivés de std::pair — chose qui doit déjà être contrainte et qui ne l'est pas en l'état — ces objets doivent également avoir la même taille et contenir les mêmes membres pour être compatibles avec le code déjà compilé.

    À ce stade, j'imagine que si l'on conserver la lignée et que l'on ne veut pas construire une std::map interne en tant que donnée-membre, il est toujours possible d'en dériver de façon privée avec class Config : private std::map mais cela nous oblige malgré tout à réimplémenter la totalité de l'interface.

    Ce qui m'amène donc à mes petites questions :

    • Est-ce que c'est possible avec std::map ?
    • À défaut, peut-on s'en sortir en ne redéfinissant que quelques membres et en considérant que les autres vont s'appuyer dessus ?
    • Sinon, même si je souhaite autant que possible ne pas recourir à une dépendance tierce, est-ce qu'il existe un équivalent dans une bibliothèque quelconque (telle que Boost) qui aurait déjà fait ce travail pour nous ?
    • Si ce problème est classique, existe-il une manière plus orthodoxe de l'aborder ?


    Merci à tous.

  2. #2
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 292
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 292
    Par défaut
    Hello,

    Le plus simple est d'étendre par l'extérieur. Et de ne pas faire un blocage sur l'écriture obt.fct() -> key(entry), value(entry).

    Maintenant, par expérience j'évite d'exposer directement des maps comme objets de configuration. Je préfère les encapsuler derrière une interface qui permet des choses comme `config.get_or_value<int>("X", 42, "extracting X coordinate")` et `config.get_or_throw<bool>("convert", "some other context");`.

    Ca permet de combiner les tâches:
    - extraction de la donnée qui renvoie au pire une valeur par défaut ou qui lancer une exception plus pertinente que `std::out_of_range("map::at()")` (ou comment perdre du temps à chercher ce qui ne va pas dans un fichier de clé=valeur)
    - conversion de la donnée qui lance une exception pertinente en cas de valeur textuelle incompatible avec le type attendu

    Après, dans mes applis lorsque j'ai des fichiers de conf, je ne suis jamais dans la situation où toutes les options sont du même type, et où je les parcours depuis le code métier
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  3. #3
    Expert éminent

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 202
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 202
    Par défaut
    La réponse est assez simple: non ou pas vraiment.

    Le destructeur de std::map est public mais non virtual.
    Cela signifie qu'on pourrait avoir un pointeur (smart ou non) de std::map qui pointe sur super_map, et un delete n'appelerait pas le destructeur de super_map.

    Par contre, On doit pouvoir ajouter un auto key(auto entry) { return std::get<0>(entry); } comme il y a déjà std::begin(), std::end()ou std::get<0>(pair/tuple)?

  4. #4
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 461
    Par défaut
    Bonjour et merci beaucoup pour ta réponse.

    Citation Envoyé par Luc Hermitte Voir le message
    Hello, Le plus simple est d'étendre par l'extérieur. Et de ne pas faire un blocage sur l'écriture obt.fct() -> key(entry), value(entry).
    Après, dans mes applis lorsque j'ai des fichiers de conf, je ne suis jamais dans la situation où toutes les options sont du même type, et où je les parcours depuis le code métier
    Oui, en fait, j'ai voulu le préciser également mais mon post était déjà assez long, donc je suis resté sur l'essentiel. Le typage des données reste secondaire dans ma situation. En l'état, les informations sont extraites du fichier texte et restent des morceaux de chaînes, que le programmeur initial traite à l'envi. Donc en ce sens, partir sur un typage fixe du style map<string, string> n'aurait pas été un problème, au moins dans un premier temps. Par contre, l'objet que j'ai écrit assure quand même un service minimum, par exemple en « trimant » les infos obtenues (suppression des blancs en début en fin de chaîne).

    Autre point qui mérite d'être souligné : j'ai appelé l'objet « config » parce que c'est bien à ça qu'il sert (parser le fichier de config et indiquer d'une manière générale les lignes à suivre au cours du programme) mais en l'état, il ne se tient qu'à l'analyse du fichier pour en mettre facilement son contenu à disposition. Il m'est arrivé de travailler sur une autre très vieille application, en Java cette fois-ci, dans laquelle j'avais bien déclaré une classe « Config » parce que l'application en était pratiquement totalement dénuée : les valeurs à suivre étaient écrites en dur pratiquement dans l'application entière et cela a demandé une phase de refactoring dédiée à ce cas en particulier. Donc, la classe rassemblait tout ce qui en tenait lieu, avec des données-membres constantes, des méthodes statiques, des pré-analyses si nécessaire et tout ce qui pouvait être utile.

    Maintenant, par expérience j'évite d'exposer directement des maps comme objets de configuration. Je préfère les encapsuler derrière une interface qui permet des choses comme `config.get_or_value<int>("X", 42, "extracting X coordinate")` et `config.get_or_throw<bool>("convert", "some other context");`.
    Si c'est « d'expérience », j'en conclus que tu as dû être confronté au même problème ou, en tout cas, à une difficulté similaire.

    Ensuite, je vois que cette approche converge en fait vers le modèle « base de registre » tel qu'il existe sous Windows ou sous « about:config » de Mozilla ou autre navigateur. C'est une bonne chose en soi, mais je voulais surtout le faire étape par étape avant d'envisager de réécrire l'application entière.

    Je voulais aussi éviter de réinventer la roue en écrivant un container si un modèle standard et répandu existait déjà, non pas par flemme mais bien pour présenter un design « propre ». Je souhaitais également conserver le bénéfice de tout ce qui a trait aux structures de données standard telles que les listes avec, notamment, la possibilité d'itérer dessus.

    Enfin, je me doutais que j'allais rencontrer ce genre de difficulté mais justement, si ça devient impossible, ça m'intéressait également d'exhiber clairement les raisons pour lesquelles ça l'est.

    Ca permet de combiner les tâches:
    - extraction de la donnée qui renvoie au pire une valeur par défaut ou qui lancer une exception plus pertinente que `std::out_of_range("map::at()")` (ou comment perdre du temps à chercher ce qui ne va pas dans un fichier de clé=valeur)
    - conversion de la donnée qui lance une exception pertinente en cas de valeur textuelle incompatible avec le type attendu
    En effet, c'est une très bonne chose parce que ça maintient le principe d'encapsulation, de « responsabilité » des données censées être fournies par l'objet et de garantie de validité une fois une certaine barrière atteinte. Cela dit, cela reste malgré tout une recherche « clé-valeur ».

    À dire vrai, c'est spécialement ce genre de méthodes que j'aurais aimé ajouter ensuite à ma classe dérivée de std::pair, tout en conservant le bénéfice d'une std::map ordinaire dans les sections de code qui n'ont pas connaissance du nôtre. Mais quoi qu'il en soit, je comprends qu'il n'y a pas de façon triviale de le faire.

    Un grand merci à tous ceux qui ont pris le temps d'y réfléchir.

  5. #5
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 060
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 060
    Par défaut
    Hello,

    Réponses courtes !


    • Est-ce que c'est possible avec std::map ? Non !
    • À défaut, peut-on s'en sortir en ne redéfinissant que quelques membres et en considérant que les autres vont s'appuyer dessus ? Non !
    • Sinon, même si je souhaite autant que possible ne pas recourir à une dépendance tierce, est-ce qu'il existe un équivalent dans une bibliothèque quelconque (telle que Boost) qui aurait déjà fait ce travail pour nous ? Non !
    • Si ce problème est classique, existe-il une manière plus orthodoxe de l'aborder ? La composition au lieu de l'héritage ?


    Réponse longue pour la dernière question,

    vous définissez votre classe Config qui possède en membre privé un std::map<Key, Value>.
    Vous exposez une API adaptée :
    • Un operator[] qui renvoie une référence au mapped_type.
    • Des fonctions begin(), end(), etc., qui retournent soit directement les itérateurs de la std::map interne, soit des itérateurs « proxy » si vous voulez vraiment forcer l’écriture entry.key / entry.value.

    Pour les alias entry.key et entry.value, deux approches sont typiques :
    • Approche A : vous laissez l’utilisateur du conteneur utiliser auto& [k, v] : config (décomposition de structure C++17), donc k et v sont nommés comme vous le désirez.
    • Approche B : vous fabriquez un petit type ConfigEntry { Key& key; Value& value; } (un proxy), que votre itérateur custom renvoie. Celui-ci référence l’élément interne d’un std::pair<const Key, Value> et expose .key et .value.


    Sachant que l'approche B ressemble à ce que vous envisagiez.

  6. #6
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 292
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 292
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Bonjour et merci beaucoup pour ta réponse.
    Si c'est « d'expérience », j'en conclus que tu as dû être confronté au même problème ou, en tout cas, à une difficulté similaire.
    [...]
    En effet, c'est une très bonne chose parce que ça maintient le principe d'encapsulation, de « responsabilité » des données censées être fournies par l'objet et de garantie de validité une fois une certaine barrière atteinte. Cela dit, cela reste malgré tout une recherche « clé-valeur ».
    [...]
    À dire vrai, c'est spécialement ce genre de méthodes que j'aurais aimé ajouter ensuite à ma classe dérivée de std::pair, tout en conservant le bénéfice d'une std::map ordinaire dans les sections de code qui n'ont pas connaissance du nôtre. Mais quoi qu'il en soit, je comprends qu'il n'y a pas de façon triviale de le faire.
    Oui. Plusieurs fois j'ai été face à des fichiers de config ou autres métadonnées stockées et dont il faut extraire des infos.

    La vision clé-valeur est simple et donc rassurante, mais elle passe à côté des ennuis que l'on va toujours avoir à un moment ou un autre. Comme je disais (autrement):
    - quid si la clé n'existe pas ? (et oui, j'ai du sortir le débuggueur pour savoir quelle méta-donnée qui n'était pas présente dans une image parce que mon appli me crachait des "erreur parce que mat::at()" -> le truc qui ne sert à rien)
    - quid si la valeur n'est pas convertible dans le type véritable de la donnée qui nous intéresse?

    Alors oui il y a la solution défensive fainéante où on se dit qu'on lancera des exceptions -- que l'on n'attrapera pas dans un premier temps parce que l'on autre chose à faire. Mais c'est une mauvaise approche. On rejoint un peu les critiques que j'avais sorties dans mes billets sur la PpC. Le meilleur moyen d'avoir un message d'erreur intelligible en cas de soucis, c'est de forcer le code utilisateur à fournir le contexte pour lequel il veut une info externe. En gros: je préfère forcer la validation des inputs au moment où je suis capable de dire ce que j'étais en train de faire.

    La vision une liste de données chaines que l'on va parcourir... non, je n'y crois pas. Ce n'est pas un vrai besoin: d'un fichier de conf que je vais juste lire. Parce que dans mes fichiers de conf, j'ai des chaines, des doubles, des listes, des entiers, des booléens et autres énums...
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  7. #7
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 461
    Par défaut
    Bonsoir et merci à tous encore une fois pour vos réponses (certains d'entre vous ont posté pendant que je rédigeais ma précédente réponse).

    Citation Envoyé par ternel Voir le message
    Le destructeur de std::map est public mais non virtual.
    Cela signifie qu'on pourrait avoir un pointeur (smart ou non) de std::map qui pointe sur super_map, et un delete n'appelerait pas le destructeur de super_map.
    Ah oui ! Effectivement, le fameux cas des destructeurs virtuels.

    Quand on y est confronté la première fois, il est toujours difficile de trouver un cas pratique qui démontre en quoi c'est un problème… Le nôtre en est un bon exemple.

    Par contre, On doit pouvoir ajouter un auto key(auto entry) { return std::get<0>(entry); } comme il y a déjà std::begin(), std::end()ou std::get<0>(pair/tuple)?
    C'est intéressant, mais pas très utile dans ma situation, car ce n'est pas plus simple que entry.first ou entry.second. Mon idée était d'agrémenter un container standard avec tout ce qui aurait pu faciliter la vie d'un développeur débutant amené à travailler sur notre jeu d'applications et également la lisibilité du code, qui peut être lu en diagonale sans avoir besoin de l'analyser explicitement. En ce sens, ce problème en particulier était surtout une question d'API management.

    Citation Envoyé par fred1599 Voir le message
    — Est-ce que c'est possible avec std::map ? Non !
    — À défaut, peut-on s'en sortir en ne redéfinissant que quelques membres et en considérant que les autres vont s'appuyer dessus ? Non !
    — Sinon, même si je souhaite autant que possible ne pas recourir à une dépendance tierce, est-ce qu'il existe un équivalent dans une bibliothèque quelconque (telle que Boost) qui aurait déjà fait ce travail pour nous ? Non !
    — Si ce problème est classique, existe-il une manière plus orthodoxe de l'aborder ? La composition au lieu de l'héritage ?
    C'est un peu ce que je voulais éviter si cela avait été possible. Comme présenté ci-dessus, à défaut de tout le reste, j'avais envisagé d'en hériter de façon privée et si là encore cela devait poser problème, de me rabattre sur une map en tant que donnée-membre privée et faire le travail moi-même.

    Écrire un container from scratch, proprement encapsulé et honorant tous les concepts qu'on peut attendre de lui, dans une très longue classe pour que, justement, le code principal reste lui le plus clair possible est extrêmement intéressant… pour l'exercice !

    Par contre, si on se retrouve contraint de réécrire la quasi-totalité d'un container standard, surtout pour n'ajouter que des références qui n'altèrent pas l'empreinte en mémoire de l'objet, on se dit d'abord que l'on doit mal s'y prendre. Les difficultés qui m'empêchaient d'avancer ici étaient bien identifiées mais comme j'avais du mal à trouver la marche à suivre ensuite, alors j'ai soumis le cas à la communauté. Ça me rassure de savoir que mes difficultés étaient justifiées.

    Réponse longue pour la dernière question,

    vous définissez votre classe Config qui possède en membre privé un std::map<Key, Value>.
    Vous exposez une API adaptée :
    • Un operator[] qui renvoie une référence au mapped_type.
    • Des fonctions begin(), end(), etc., qui retournent soit directement les itérateurs de la std::map interne, soit des itérateurs « proxy » si vous voulez vraiment forcer l’écriture entry.key / entry.value.
    Oui mais l'interface de std::map compte pas moins de trente-sept fonctions-membres et près d'une dizaine d'opérateurs de comparaison externes. Même en considérant l'objet constant après analyse du fichier pour se passer des modifiers, il faudrait quand même en honorer une bonne moitié (par exemple count(), find() ou at()).

    Et quitte à faire ce travail en entier, on peut alors choisir de re-dériver publiquement d'une std::map<string, string> dans lesquels les types template sont fixes car on prendrait en charge les proxys dans chacune de nos fonctions-membres redéfinies.

    Pour les alias entry.key et entry.value, deux approches sont typiques :
    • Approche A : vous laissez l’utilisateur du conteneur utiliser auto& [k, v] : config (décomposition de structure C++17), donc k et v sont nommés comme vous le désirez.
    • Approche B : vous fabriquez un petit type ConfigEntry { Key& key; Value& value; } (un proxy), que votre itérateur custom renvoie. Celui-ci référence l’élément interne d’un std::pair<const Key, Value> et expose .key et .value.


    Sachant que l'approche B ressemble à ce que vous envisagiez.
    C'est à cela que je pensais.

    C'est pour cela que je me demande pourquoi std::map impose l'usage de std::pair pour ses entrées et ne permet pas l'usage direct de tels proxys. Le type des entrées pourrait même être directement déduit du paramètre template Allocator (le quatrième, défini comme « std::pair » par défaut) puisque c'est exactement ce que fait l'assertion statique qui vérifie qu'il est dans les clous. Le membre « value_type » qui en fait est juste un typedef et pas une donnée-membre à proprement parler pourrait l'être de la même façon, surtout qu'on peut le redéfinir nous-mêmes dans notre classe dérivée. Le problème du destructeur non virtuel reste identique même avec la définition actuelle de std::map.

    Certes, il y a quand même deux problèmes avec cette approche. Le premier est qu'il faut s'assurer d'utiliser un objet qui rendent les mêmes services que std::pair mais ça, c'est à la responsabilité du développeur. Le suivant est que cela obligerait la classe à être template dans sa quasi-totalité si on ne distingue pas la manipulation des objets elle-même de l'algorithme de classement mais là encore, std::pair est elle-même template, donc le problème reste globalement le même.

    Citation Envoyé par Luc Hermitte Voir le message
    Oui. Plusieurs fois j'ai été face à des fichiers de config ou autres métadonnées stockées et dont il faut extraire des infos.

    La vision clé-valeur est simple et donc rassurante, mais elle passe à côté des ennuis que l'on va toujours avoir à un moment ou un autre. Comme je disais (autrement):
    - quid si la clé n'existe pas ? (et oui, j'ai du sortir le débuggueur pour savoir quelle méta-donnée qui n'était pas présente dans une image parce que mon appli me crachait des "erreur parce que mat::at()" -> le truc qui ne sert à rien)
    - quid si la valeur n'est pas convertible dans le type véritable de la donnée qui nous intéresse?
    Entièrement d'accord, et spécialement avec le fait que cela doit être effectivement la responsabilité de l'objet. Mais là encore, c'est censé être « l'étape d'après ». Et en particulier, autant le lancer d'exception en cas d'absence devrait être de la responsabilité de la map (ou « du mapping »), autant la conversion de type ou le choix d'une valeur par défaut devrait être celle de la pair… d'où l'intérêt de pouvoir redéfinir cette dernière.

    Alors oui il y a la solution défensive fainéante où on se dit qu'on lancera des exceptions -- que l'on n'attrapera pas dans un premier temps parce que l'on autre chose à faire. Mais c'est une mauvaise approche. On rejoint un peu les critiques que j'avais sorties dans mes billets sur la PpC. Le meilleur moyen d'avoir un message d'erreur intelligible en cas de soucis, c'est de forcer le code utilisateur à fournir le contexte pour lequel il veut une info externe. En gros: je préfère forcer la validation des inputs au moment où je suis capable de dire ce que j'étais en train de faire.
    On est sur la même longueur d'onde, je pense. Ça rejoint un peu mes histoires de barrières un peu plus haut.

    La vision une liste de données chaines que l'on va parcourir... non, je n'y crois pas. Ce n'est pas un vrai besoin: d'un fichier de conf que je vais juste lire. Parce que dans mes fichiers de conf, j'ai des chaines, des doubles, des listes, des entiers, des booléens et autres énums...
    Effectivement, on touche du doigt les grandes lignes de la conception d'un modèle propre : si on considère qu'un objet « Config » a pour mission de donner la valeur d'un paramètre nommé, alors l'opérateur operator [] suffit et peu importe la structure de données que l'on utilise pour les conserver. Et si celle-ci est purement privée, on peut mettre se réserver le luxe de la faire varier d'une version à l'autre.

    Par contre, ce ne doit pas être un « prétexte » pour classer d'emblée les difficultés techniques exposées et surtout, il était important d'en discuter collégialement. Si en outre, on prend le problème à l'envers en se demandant « quelle est la structure de données la plus appropriée pour lire un fichier de configuration en mode texte contenant des entrées de forme « CLÉ = VALEUR », alors sauf erreur, c'est la map qui reste la plus appropriée.

    Si en outre, le programmeur voit son propre fichier comme une liste de paires clés-valeurs (ce qu'il est), alors il va vouloir avoir la possibilité de le manipuler comme une liste et notamment, de pouvoir le parcourir, ne serait-ce que pour savoir ce qu'il contient, et ce même si les noms des clés sont théoriquement tous connus du programme à l'avance (ce qui n'est même pas forcément le cas).

    Le cas typique étant justement le programme d'exemple que j'avais mis en place, à la fois pour en démontrer le bon fonctionnement et pour montrer que cela se fait en quelques lignes :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
        cout << "Contenu du fichier de configuration :" << endl << endl;
     
        for(entry : config)
            cout << "La clé " << entry.key << " contient " << entry.value << endl;
    Ce tout petit extrait directement déposé dans main() produit par définition une sortie écran dont le contenu est similaire à celui du fichier et permet de vérifier en un seul coup d'œil que les entrées ont été correctement traitées, voire converties si nécessaires.

  8. #8
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    760
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 760
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Ah oui ! Effectivement, le fameux cas des destructeurs virtuels.

    Quand on y est confronté la première fois, il est toujours difficile de trouver un cas pratique qui démontre en quoi c'est un problème… Le nôtre en est un bon exemple.
    En réalité, c'est un problème uniquement si des variables membres sont ajoutées. Et encore, si on ajoute des variables qui ne font rien dans le destructeur, ce n'est pas forcément un problème (mais ça dépend de l'allocateur utilisé, du coup c'est à risque).

    Citation Envoyé par Obsidian Voir le message
    Oui mais l'interface de std::map compte pas moins de trente-sept fonctions-membres et près d'une dizaine d'opérateurs de comparaison externes. Même en considérant l'objet constant après analyse du fichier pour se passer des modifiers, il faudrait quand même en honorer une bonne moitié (par exemple count(), find() ou at()).
    Je ne compte pas autant de fonction membre et encore moins d'opérateur de comparaison (surtout quand il n'y a que 6 opérateurs de comparaisons existant, ou 7 en C++20, mais dont un en recouvre 5). Après je n'ai pas regardé les multi-prototypes.

    Par contre, pour un objet constant il n'y a clairement pas besoin d'autant si on regarde dans le détail:

    - opérateur de comparaison: je n'en vois pas l’intérêt, limite garder == et !=, mais c'est spécial. Surtout que l'objet définit des valeurs par défaut, ça devient très étrange. Dans pas mal de scénario, l'absence de valeur n'est pas équivalent à une valeur par défaut.
    - contains(): ok
    - count(): c'est un contains qui retourne 0 ou 1, bof (pour moi c'est là afin d'être compatible avec std::multimap).
    - find(): ok
    - at(): un find qui balance une exception sans contexte, bof
    - operator[]: est un mutateur
    - begin(), end(), rbegin(), rend(): mhouais, encore faut-il un vrai cas d'usage (surtout pour le reverse)
    - empty() / size(): est-ce utile pour une config ?
    - get_allocator(), max_size(), key_comp(), value_comp(): déjà que pour trouver un cas d'usage avec std::map, c'est pas facile, alors un truc spécialisé...
    - equal_range(), lower_bound(), upper_bound(): ce sont des propriétés d'ordre, ça me parait très étrange dans un objet de config.

    Si on restreint à ce qui est vraiment utile pour une config, ça fait pas lourd.

    Sinon, avec un héritage privé sur std::map, toutes les autres fonctions s'exportent dans l'interface public avec des using std::map::machin. Si ton problème se situe uniquement sur l'interface de std::pair, tu as toujours moyen de redéfinir uniquement les fonctions qui les retournent pour, à la place, retourner ton propre type qui wrap un pointeur aur std::pair.

    Mais tu peux aussi remplacer tous les accesseurs par une interface plus pratique. Une fonction get(k) qui retourne un objet intermédiaire avec toutes les fonctions qui vont bien pour savoir si la clef est présente et faire les transformations de type pour les valeurs.


    Citation Envoyé par Obsidian Voir le message
    C'est pour cela que je me demande pourquoi std::map impose l'usage de std::pair pour ses entrées et ne permet pas l'usage direct de tels proxys. Le type des entrées pourrait même être directement déduit du paramètre template Allocator (le quatrième, défini comme « std::pair » par défaut) puisque c'est exactement ce que fait l'assertion statique qui vérifie qu'il est dans les clous.
    C'est vrai que changer le type interne serait assez sympa. Théoriquement, std::map pourrait passer par un trait pour s'interfacer avec ce type et tout deviendrait transparent vis à vis des accès. Je pense que cela n'a juste pas été pensé.


    Citation Envoyé par Obsidian Voir le message
    « quelle est la structure de données la plus appropriée pour lire un fichier de configuration en mode texte contenant des entrées de forme « CLÉ = VALEUR », alors sauf erreur, c'est la map qui reste la plus appropriée.
    ou std::unordered_map.


    Si en outre, le programmeur voit son propre fichier comme une liste de paires clés-valeurs (ce qu'il est), alors il va vouloir avoir la possibilité de le manipuler comme une liste et notamment, de pouvoir le parcourir, ne serait-ce que pour savoir ce qu'il contient, et ce même si les noms des clés sont théoriquement tous connus du programme à l'avance (ce qui n'est même pas forcément le cas).
    Personnellement, je ne vois pas de cas d'usage réel où le programmeur voudrait faire une telle chose. Je ne vois que l'utilitaire (donc pour l'usagé) qui liste les valeurs de la config. Mais il n'y a pas besoin de manipuler une liste, on peut très bien imaginer un for_each(f) qui appel f manuellement sur toutes les clef / valeur.

    J'ai aussi beaucoup de mal à imaginer une config où tout n'est pas connu à l'avance.

    À chaque fois que j'ai eu à faire des objets de config, il y a systématiquement eu derrière une classe avec de vrais membres bien typés comme il faut. Du coup, plutôt que faire map -> struct après parsing, je déplace la construction de la structure dans la phase de mapping (dans la forme très simple, le parseur passe par une callback en refilant clef/valeur).

    Je trouve que cette approche a énormément d'avantage à l'usage:

    - Comme la config connaît toutes les clefs, on peut indiquer très tôt celles inconnues. On peut même lister les noms proches pour aider l'utilisateur.
    - Comme les types des valeurs sont connus à l'avance, on peut indiquer très tôt les erreurs de syntaxe d'une valeur. Avec le numéro ligne. Essentiel quand le parseur autorise les fichiers avec plusieurs fois la même clef.
    - Comme la transformation des types et plus proche du parsing, la tendance à faire des parseurs plus riches vient plus facilement. Par exemple, ajouter la notion d'unité pour les valeurs représentant des délais. Cela uniformise aussi les parseurs.
    - Tout ce qui représente la config est regroupé au même endroit plutôt que dispatcher de-ci de-là. Je trouve cela plus pratique pour la doc.
    - À l'usage, aucune possibilité de mal écrire la clef, c'est vérifié par le compilateur

  9. #9
    Expert éminent

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 202
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 202
    Par défaut
    Quitte à aller dans cette direction, regarde boost.config.

    Non pas que ce soit ta solution, mais boost.config a une api plus métier (et gère les arborescences de configuration, les options runtime, la lecture d'un fichier de conf xml ou autre)

    En fait, ce qui me gène depuis le début dans cette histoire, c'est que tu essaies d'utiliser une classe beaucoup trop "technique" pour un besoin "métier".
    std::map est moyen de stocker un dictionnaire, et une configuration est une forme de dictionnaire.

    Par exemple, la réalité, c'est qu'en principe, les options de l'application sont une liste fixe. Il n'y a pas forcément besoin d'une api de recherche.

    Si l'application à un "port d'écoute", la feature, ce n'est pas app.config.get("LISTENING_PORT") (oups, c'est quoi la constante, ah oui, app.config.get(app::connection_manager::config::listening_port_key)))

    C'est app::config::listening_port() ou app::connection_manager.listening_port().

    Du coup, la map est caché par cette api métier. Qu'on utilise map, unordered_map, flat_map, boost.config ou autre chose encore, peut importe, c'est encapsulé derrière un vocabulaire métier.

    Si on veut répartir les fonctions de configuration dans plusieurs classes configurables, alors en gros, il faut qu'elles aient accès a une api d'accès à la config, qui va effectivement ressembler à ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    class config {
    // template ou non, à voir. on peut très bien avoir getInt, getLong et getString.
    template <typename T> T getOrThrow(config::key_type const&) const;
    template <typename T> T getOrFallback(config::key_type const&, T const& defaultValue) const;
    template <typename T> std::optional<T> get(config::key_type const&) const;
    La grosse question, c'est où cette classe se trouve entre:
    • le fichier de config ou les arguments de ligne de commande (tas de caractères)
    • le contenu analysé (string -> string)
    • le contenu vérifié et parsé (string -> types)
    • l'option de la classe métier (tas de variables)
    • l'api de la classe métier (fonction -> valeur métier)


    La classe de configuration n'aura pas du tout le même role à chaque étape.

  10. #10
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 292
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 292
    Par défaut
    On se rejoint avec mes petits camarades. J'aime bien l'idée du for-each qui va prendre une lambda de visitation -- il me semble avoir fait un truc comme ça je ne sais plus où d'ailleurs.

    Et tout cela ne peut que m'évoquer ce principe énoncé par Scott Meyers: "Make Interfaces Easy to Use Correctly and Hard to Use Incorrectly" https://www.oreilly.com/library/view...9515/ch55.html
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  11. #11
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 461
    Par défaut
    Citation Envoyé par jo_link_noir Voir le message
    Si on restreint à ce qui est vraiment utile pour une config, ça fait pas lourd.

    Sinon, avec un héritage privé sur std::map, toutes les autres fonctions s'exportent dans l'interface public avec des using std::map::machin. Si ton problème se situe uniquement sur l'interface de std::pair, tu as toujours moyen de redéfinir uniquement les fonctions qui les retournent pour, à la place, retourner ton propre type qui wrap un pointeur aur std::pair.
    C'est effectivement à ça que je pensais également. En tout cas c'est l'approche « optimale » vers laquelle j'ai convergé, mais il y avait beaucoup de choses à vérifier d'abord. Et comme expliqué, je voulais surtout être sûr de n'avoir rien loupé moi-même avant, donc j'ai exposé le cas à la communauté.

    Mais tu peux aussi remplacer tous les accesseurs par une interface plus pratique. Une fonction get(k) qui retourne un objet intermédiaire avec toutes les fonctions qui vont bien pour savoir si la clef est présente et faire les transformations de type pour les valeurs.
    Oui, mais je voulais que ce soit transparent au niveau du code. Quite à introduire de la « complexité », ou à tout le moins une interface atypique, autant remettre le problème entier à plat et concevoir d'emblée une classe adaptée à ce travail comme suggéré au fil de la discussion (ce qui est d'ailleurs ce que j'ai fait pour d'autres projets).

    Personnellement, je ne vois pas de cas d'usage réel où le programmeur voudrait faire une telle chose. Je ne vois que l'utilitaire (donc pour l'usagé) qui liste les valeurs de la config. Mais il n'y a pas besoin de manipuler une liste, on peut très bien imaginer un for_each(f) qui appel f manuellement sur toutes les clef / valeur.

    J'ai aussi beaucoup de mal à imaginer une config où tout n'est pas connu à l'avance.

    Citation Envoyé par ternel Voir le message
    Quitte à aller dans cette direction, regarde boost.config.

    Non pas que ce soit ta solution, mais boost.config a une api plus métier (et gère les arborescences de configuration, les options runtime, la lecture d'un fichier de conf xml ou autre)

    En fait, ce qui me gène depuis le début dans cette histoire, c'est que tu essaies d'utiliser une classe beaucoup trop "technique" pour un besoin "métier".
    std::map est moyen de stocker un dictionnaire, et une configuration est une forme de dictionnaire.
    En fait, c'est pour cela que j'ai parlé de problème XY en début de fil. Il y avait trois raisons principales à cela :

    • Je refactore une application dans laquelle le programmeur utilise déjà ce modèle, en parsant un fichier de paramètres. Soit il utilise un gros while qui contient une série de if pour vérifier si, à chaque ligne lue, le paramètre correspond à un des cas connus, soit il dépose le tout dans une table. Donc, je voulais déjà lui faire ce travail en déposant le tout mis au propre dans un container standard ;
    • Il s'agit en fait ici d'une lecture de paramètres, et ce type de fichier peut être utilisé dans d'autres contextes que la configuration initiale, même si c'est ce cas de figure qui m'a fait me pencher dessus (non seulement parce que c'est le premier rencontré, mais aussi parce qu'il réitère le même travail au début de chacun de ses exécutables) ;
    • Ce cas était intéressant sur le plan strictement technique et méritait d'être soumis. En plus, si écrire une classe métier est en général l'approche à suivre, il ne faut pas que cela soit une solution de rabattement due au fait que le problème initial était trop difficile…


    La grosse question, c'est où cette classe se trouve entre:
    • le fichier de config ou les arguments de ligne de commande (tas de caractères)
    • le contenu analysé (string -> string)
    • le contenu vérifié et parsé (string -> types)
    • l'option de la classe métier (tas de variables)
    • l'api de la classe métier (fonction -> valeur métier)
    En l'état des choses, entre le deuxième et le troisième point. C'est-à-dire que cela reste un stringstring parce qu'il s'agit d'informations saisies par l'utilisateur (et que, comme expliqué plus haut, même si le premier et seul cas d'usage rencontré actuellement est celui du fichier de config, je n'exclus pas d'en rencontrer ailleurs), donc je ne les convertis pas sur le fond parce qu'il manque le contexte, mais je les adapte quand même sur la forme si nécessaire, principalement en trimant les blancs de début et fin de ligne (et c'est à peu près tout à ce stade).

    En tout cas, je crois que l'on en a fait le tour, en confirmant que ce n'était pas possible, en montrant pourquoi, et en constatant que l'on est globalement d'accord sur la meilleure approche à suivre pour implémenter ce cas, ou sur la manière dont il faudrait concevoir une classe dédiée si c'était à refaire.

    Je marque donc le sujet résolu.
    Un grand merci à tous !

  12. #12
    Membre Expert
    Profil pro
    Inscrit en
    Juillet 2006
    Messages
    1 493
    Détails du profil
    Informations personnelles :
    Localisation : France, Paris (Île de France)

    Informations forums :
    Inscription : Juillet 2006
    Messages : 1 493
    Par défaut
    Hello,

    Le design pattern Decorator pourrait-il s'appliquer ?

  13. #13
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 461
    Par défaut
    Citation Envoyé par deedolith Voir le message
    Hello, Le design pattern Decorator pourrait-il s'appliquer ?
    Bonjour et merci pour ta remarque.
    Je ne les utilise que très rarement (en partie parce que j'ai du mal à trouver des projets qui me permettent de pratiquer, mais c'est un autre problème) mais je ne pense pas que ce soit utile, ici.

    D'abord parce qu'il n'y a pas de manière simple de les implémenter en C++. Donc ici, on ne les reconnaîtrait qu'en tant que motif de conception et cela ne nous faciliterait pas beaucoup la tâche sur le plan technique. Ensuite, parce que ce patron lui-même ne serait pas un motif générique que l'on pourrait appliquer indifféremment à tout type d'objet, et enfin parce que cela ne résoudrait pas notre problème de fond (impossibilité d'étendre les std::pair sous-jacentes).

    Par contre, pour revenir très brièvement sur le problème initial. La « configuration » était tirée d'un fichier *.ini écrit directement par l'utilisateur. Ce n'en était pas vraiment un dans le sens des fichiers de configuration de Windows ou de Git parce qu'il ne contenait que des paires clés-valeur en texte et pas de blocs [section] par exemple, mais cela reste intéressant d'écrire un objet dédié pour les lire en une fois, faire le traitement nécessaire sur les chaînes et être capable d'ignorer les lignes vides comme de reconnaître les commentaires #. Donc en ce sens, il s'agit surtout ici d'écrire une classe FichierINI qui, elle-même, peut ensuite être directement dérivée en classe Config si nécessaire ou faire en sorte que cette dernière s'appuie sur elle en tant que membre privé.

    Mais avant même cela, tout le problème s'articule autour du fait que std::pair n'est pas accessible alors que l'on a conclu ici que cela aurait tout-à-fait possible et probablement souhaitable.

    Et il se trouve que, sans réinventer la roue (chose à laquelle il faut faire particulièrement attention en C++), la bibliothèque d'objets que j'écris pour le projet concerné contient beaucoup d'objet std:: étendus pour les besoins ponctuels. Donc quitte à réimplémenter la majorité de l'interface ou à l'importer avec using, je pense qu'il devient justifié d'écrire une vraie myproject::map dérivant du container standard. Sans compter que cela permettra de la faire évoluer par la suite plus facilement encore que si elle était cantonnée au seul cas d'usage exposé ici.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 4
    Dernier message: 16/01/2012, 18h26
  2. map<string, MaClasse<T>*> est ce possible ?
    Par julie_n3k0 dans le forum C++
    Réponses: 4
    Dernier message: 03/09/2009, 00h11
  3. std::map<Etat,Position> est-ce posible ?
    Par tigger_riric dans le forum SL & STL
    Réponses: 10
    Dernier message: 06/05/2007, 15h01
  4. std::map tester si une clé est présente
    Par mister3957 dans le forum SL & STL
    Réponses: 2
    Dernier message: 08/04/2006, 12h31

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo