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.
Partager