Depuis http://www.cplusplus.com/reference/utility/pair/ , nous savons que std::pair
A deux variables membres, first
et second
.
Pourquoi les concepteurs STL ont-ils décidé d'exposer deux variables membres, first
et second
, au lieu de proposer une getFirst()
et une getSecond()
?
Pour le C++ 03 d'origine std::pair
, Les fonctions d'accès aux membres ne serviraient à rien.
À partir de C++ 11 et versions ultérieures (nous sommes maintenant à C++ 14, avec C++ 17 à venir rapidement) std::pair
Est un cas spécial de std::Tuple
, Où std::Tuple
Peut avoir n'importe quel nombre d'articles. En tant que tel, il est logique d'avoir un getter paramétré, car il ne serait pas pratique d'inventer et de normaliser un nombre arbitraire de noms d'éléments. Ainsi, vous pouvez utiliser std::get
également pour un std::pair
.
Ainsi, les raisons de la conception sont historiques, que le std::pair
Actuel est le résultat final d'une évolution vers plus de généralité.
Dans d'autres nouvelles:
en ce qui concerne
” Pour autant que je sache, il serait préférable d'encapsuler deux variables membres ci-dessus et de donner une fonction
getFirst();
etgetSecond()
non, ce sont des ordures.
C'est comme dire qu'un marteau est toujours mieux, que vous enfonçiez des clous, fixiez avec des vis ou que vous coupiez un morceau de bois. Surtout dans le dernier cas, un marteau n'est tout simplement pas un outil utile. Les marteaux peuvent être très utiles, mais cela ne signifie pas qu'ils sont "meilleurs" en général: c'est juste un non-sens.
Les getters et setters sont généralement utiles si l'on pense que l'obtention ou la définition de la valeur nécessite une logique supplémentaire (modification d'un état interne). Cela peut ensuite être facilement ajouté à la méthode. Dans ce cas std::pair
n'est utilisé que pour fournir 2 valeurs de données. Ni plus ni moins. Et donc, ajouter la verbosité d'un getter et d'un setter serait inutile.
La raison en est qu'aucun réel invariant ne doit être imposé à la structure des données, comme std::pair
modélise un conteneur à usage général pour deux éléments. En d'autres termes, un objet de type std::pair<T, U>
est supposé être valide pour tout élément first
et second
possible de type T
et U
, respectivement. De même, les mutations ultérieures de la valeur de ses éléments ne peuvent pas vraiment affecter la validité du std::pair
en soi.
Alex Stepanov (l'auteur de la STL) présente explicitement ce principe général de conception pendant son cours Programmation efficace avec des composants , en commentant sur le conteneur singleton
(c'est-à-dire un conteneur d'un élément).
Ainsi, bien que le principe en soi puisse être une source de débat, c'est la raison derrière la forme de std::pair
.
Les getters et setters sont utiles si l'on pense que l'abstraction est justifiée pour isoler les utilisateurs des choix de conception et des changements de ces choix, maintenant ou à l'avenir.
L'exemple typique de "maintenant" est que le setter/getter peut avoir une logique pour valider et/ou calculer la valeur - par exemple, utiliser un setter pour un numéro de téléphone, au lieu d'exposer directement le champ, afin que vous puissiez vérifier le format; utiliser un getter pour une collection afin que le getter puisse fournir une vue en lecture seule de la valeur du membre (une collection) à l'appelant.
L'exemple canonique ( bien que mauvais ) pour "les changements dans le futur" est Point
- si vous exposez la fonction x
et y
ou getX()
et getY()
? La réponse habituelle est d'utiliser des getters/setters car à un moment donné dans le futur, vous voudrez peut-être changer la représentation interne de cartésienne en polaire et vous ne le faites pas ' Je ne veux pas que vos utilisateurs soient impactés (ou qu'ils dépendent de cette décision de conception).
Dans le cas de std::pair
- l'intention est que cette classe représente maintenant et pour toujours deux et exactement deux valeurs (de type arbitraire) et fournisse leurs valeurs à la demande. C'est ça. Et c'est pourquoi la conception utilise l'accès direct aux membres, plutôt que de passer par un getter/setter.
On pourrait soutenir que std::pair
serait mieux d'avoir des fonctions d'accesseur pour accéder à ses membres! Notamment pour les cas dégénérés de std::pair
il pourrait y avoir un avantage. Par exemple, quand au moins l'un des types est une classe vide et non finale, les objets peuvent être plus petits (la base vide peut être transformée en une base qui n'aurait pas besoin d'obtenir sa propre adresse).
À l'époque std::pair
a été inventé, ces cas particuliers n'ont pas été pris en compte (et je ne sais pas si l'optimisation de la base vide a été autorisée dans le projet de document de travail à l'époque). D'un point de vue sémantique, il n'y a pas beaucoup de raisons d'avoir des fonctions d'accesseur, cependant: il est clair que les accesseurs devraient renvoyer une référence mutable pour les objets nonconst
. En conséquence, l'accesseur ne fournit aucune forme d'encapsulation.
D'un autre côté, cela rend [légèrement] plus difficile pour l'optimiseur de voir ce qui se passe lorsque les fonctions d'accesseur sont utilisées, par ex. car des points de séquence supplémentaires sont introduits. Je pourrais imaginer que Meng Lee et Alexander Stepanov ont même mesuré s'il y a une différence (moi non plus). Même s'ils ne l'ont pas fait, fournir l'accès aux membres directement n'est certainement pas plus lent que de passer par une fonction d'accesseur alors que l'inverse n'est pas nécessairement vrai.
Je ne faisais pas partie de la décision et la norme C++ n'a pas de justification mais je devinez c'était une décision délibérée de rendre les membres publics membres des données.
Le but principal des getters et setters est de prendre le contrôle de l'accès. C'est-à-dire que si vous exposez "first" comme variable, n'importe quelle classe peut la lire et l'écrire (sinon const
) sans lui dire qu'elle fait partie de. Dans un certain nombre de cas, cela peut poser de graves problèmes.
Par exemple, supposons que vous ayez une classe qui représente le nombre de passagers sur un bateau. Vous stockez le nombre de passagers sous forme d'entier. Si vous exposez ce nombre en tant que variable nue, il serait possible que des fonctions externes y écrivent. Cela pourrait vous laisser dans un cas où il y a en fait 10 passagers, mais quelqu'un a changé la variable (peut-être accidentellement) pour qu'elle soit 50. C'est le cas pour un getter sur le nombre de passagers (mais pas un setter, qui présenterait le - même problème).
Un exemple pour les getters et les setters serait une classe qui représente un vecteur mathématique dans lequel vous souhaitez mettre en cache certaines informations sur le vecteur. Supposons que vous souhaitiez enregistrer la longueur. Dans ce cas, la modification de vec.x
changerait probablement la longueur/magnitude. Donc, non seulement vous devez faire en sorte que x soit enveloppé dans un getter, vous devez également fournir un setter pour x, qui sait mettre à jour la longueur en cache du vecteur. (Bien sûr, la plupart des bibliothèques mathématiques actuelles ne mettent pas ces valeurs en cache et exposent donc les variables.)
Donc, la question que vous devez vous poser dans le contexte de leur utilisation est: cette classe jamais va-t-elle devoir contrôler ou être alertée des changements de cette variable?
La réponse dans quelque chose comme std :: pair est un "non" plat. Il n'y a pas lieu de contrôler l'accès aux membres d'une classe dont le seul but est de contenir ces membres. Il n'est certainement pas nécessaire que la paire sache si ces variables ont été touchées, étant donné qu'il s'agit de ses deux seuls membres, et qu'elle n'a donc pas d'état à mettre à jour si elle change. la paire ignore ce qu'elle contient et sa signification, donc suivre ce qu'elle contient ne vaut pas la peine.
Selon le compilateur et la façon dont il est configuré, les getters et setters peuvent introduire une surcharge. Ce n'est probablement pas important dans la plupart des cas, mais si vous les mettiez sur quelque chose de fondamental comme std::pair
, ce serait une préoccupation non triviale. En tant que tel, leur ajout devrait être justifié - ce qui, comme je viens de l'expliquer, ne peut pas l'être.
J'ai été consterné par le nombre de commentaires qui ne montrent aucune compréhension de base de la conception orientée objet (cela prouve-t-il que le c ++ n'est pas un langage OO?). Oui, la conception de std :: pair a quelques traits historiques, mais cela ne rend pas une bonne conception mauvaise; il ne doit pas non plus être utilisé comme excuse pour nier le fait. Avant de m'énerver, permettez-moi de répondre à certaines des questions dans les commentaires:
Ne pensez-vous pas que l'int devrait aussi avoir un setter et un getter
Oui, du point de vue de la conception, nous devons utiliser des accesseurs car, ce faisant, nous ne perdons rien mais gagnons une flexibilité supplémentaire. Certains algorithmes plus récents peuvent vouloir intégrer des bits supplémentaires dans la clé/les valeurs, et vous ne pouvez pas les coder/décoder sans accesseurs.
Pourquoi envelopper quelque chose dans un getter s'il n'y a pas de logique dans le getter?
Comment savez-vous qu'il n'y aurait pas de logique dans le getter/setter? Une bonne conception ne doit pas limiter la possibilité de mise en œuvre basée sur une supposition. Il devrait offrir autant de flexibilité que possible. Rappelez-vous que la conception de std: pair décide également de la conception de l'itérateur, et en obligeant les utilisateurs à accéder directement aux variables membres, l'itérateur doit renvoyer des structures qui stockent réellement les clés/valeurs ensemble. Cela s'avère être une grosse limitation. Il existe des algorithmes qui doivent les séparer. Il existe des algorithmes qui ne stockent pas du tout les clés/valeurs de manière explicite. Maintenant, ils doivent copier les données pendant l'itération.
Contrairement à la croyance populaire, avoir des objets qui ne font que stocker des variables membres avec des getters et setters n'est pas "la façon dont les choses devraient être faites"
Une autre supposition sauvage.
OK, je m'arrêterais ici.
Pour répondre à la question d'origine: std :: pair a choisi d'exposer les variables membres car celui qui l'a conçu n'a pas reconnu et/ou priorisé l'importance d'un contrat flexible. Ils avaient évidemment une idée/vision très étroite sur la façon dont les paires clé-valeur dans une carte/table de hachage devraient être implémentées, et pour empirer les choses, ils ont laissé une vue aussi étroite sur l'implémentation déborder par le haut pour compromettre la conception. Par exemple, que se passe-t-il si je souhaite implémenter un remplacement de std: unordered_map qui stocke les clés et les valeurs dans des tableaux distincts basés sur un schéma d'adressage ouvert avec sondage linéaire? Cela peut considérablement améliorer les performances du cache pour les paires avec de petites clés et de grandes valeurs, car vous n'avez pas besoin de sauter à travers les espaces occupés par les valeurs pour sonder les clés. Si std :: pair avait choisi des accesseurs, il serait trivial d'écrire un itérateur de style STL pour cela. Mais maintenant, il est tout simplement impossible d'y parvenir sans obtenir une copie supplémentaire des données.
J'ai remarqué qu'ils prescrivent également l'utilisation du hachage ouvert (c'est-à-dire le chaînage fermé) pour la mise en œuvre de std :: unordered_map. Ce n'est pas seulement étrange du point de vue de la conception (pourquoi vous voulez restreindre la façon dont les choses sont implémentées ???), mais aussi assez stupide en termes d'implémentation - les tables de hachage chaînées utilisant une liste liée sont peut-être la plus lente de toutes les catégories. Allez sur Google sur le Web, nous pouvons facilement trouver que std: unordered_map est souvent le paillasson d'une référence de table de hachage. Il a même tendance à être plus lent que HashMap de Java (je ne sais pas comment ils ont réussi à prendre du retard dans ce cas, car HashMap est également une table de hachage enchaînée). Une vieille excuse est que la table de hachage chaînée a tendance à mieux fonctionner lorsque le facteur de charge approche 1, ce qui est totalement invalide car 1) il existe de nombreuses techniques dans la famille d'adressage ouvert pour faire face à ce problème - jamais entendu parler de marelle ou de hachage par capotage, et ce dernier est en fait là depuis 30 années fantastiques; 2) une table de hachage enchaînée ajoute la surcharge d'un pointeur (un bon 8 octets sur les machines 64 bits) pour chaque entrée, donc quand nous disons que le load_factor d'un unordered_map approche 1, ce n'est pas 100% d'utilisation de la mémoire! Nous devons prendre cela en considération et comparer les performances de unordered_map avec des alternatives avec la même utilisation de la mémoire. Et il s'avère que des alternatives comme Google Dense HashMap sont 3-4 fois plus rapides que std :: unordered_map.
Pourquoi ces informations sont-elles pertinentes? Parce qu'intéressant, rendre obligatoire le hachage ouvert rend la conception de std :: pair moins mauvaise, maintenant que nous n'avons pas besoin de la flexibilité d'une structure de stockage alternative. De plus, la présence de std :: pair rend presque impossible l'adoption d'algorithmes plus récents/meilleurs pour écrire un remplacement direct de std :: unordered_map. Parfois, vous vous demandez s'ils l'ont fait intentionnellement afin que la mauvaise conception de std :: pair et l'implémentation piétonne de std :: unordered_map puissent survivre ensemble plus longtemps. Bien sûr, je plaisante, donc celui qui a écrit ça, ne vous offusquez pas. En fait, les gens qui utilisent Java ou Python (OK, j'avoue que Python est un tronçon)) voudraient vous remercier de leur avoir fait du bien d'être "aussi vite que C++ ".