Dois-je préférer des pointeurs ou des références dans les données des membres?


138

Voici un exemple simplifié pour illustrer la question:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Donc B est responsable de la mise à jour d'une partie de C. J'ai fait passer le code à travers lint et il s'est plaint du membre de référence: lint # 1725 . Cela parle de prendre soin de la copie et des affectations par défaut, ce qui est assez juste, mais la copie et l'affectation par défaut sont également mauvaises avec les pointeurs, donc il n'y a que peu d'avantages.

J'essaie toujours d'utiliser des références là où je peux, car les pointeurs nus introduisent une incertitude quant à savoir qui est responsable de la suppression de ce pointeur. Je préfère incorporer des objets par valeur, mais si j'ai besoin d'un pointeur, j'utilise auto_ptr dans les données membres de la classe qui possède le pointeur, et je passe l'objet comme référence.

Je n'utiliserais généralement un pointeur dans les données des membres que lorsque le pointeur pourrait être nul ou changer. Y a-t-il d'autres raisons de préférer les pointeurs aux références pour les membres de données?

Est-il vrai de dire qu'un objet contenant une référence ne doit pas être assignable, puisqu'une référence ne doit pas être modifiée une fois initialisée?


"mais la copie et l'affectation par défaut sont également mauvaises avec les pointeurs": Ce n'est pas la même chose: un pointeur peut être changé quand vous le souhaitez s'il n'est pas constant. Une référence est normalement toujours const! (Vous verrez cela si vous essayez de changer votre membre en "A & const a;". Le compilateur (au moins GCC) vous avertira que la référence est de toute façon const même sans le mot-clé "const".
mmmmmmmm

Le principal problème avec ceci est que si quelqu'un fait B b (A ()), vous êtes foutu parce que vous vous retrouvez avec une référence pendante.
log0

Réponses:


67

Évitez les membres de référence, car ils restreignent ce que l'implémentation d'une classe peut faire (y compris, comme vous le mentionnez, empêcher l'implémentation d'un opérateur d'affectation) et n'apportent aucun avantage à ce que la classe peut fournir.

Exemples de problèmes:

  • vous êtes obligé d'initialiser la référence dans la liste d'initialisation de chaque constructeur: il n'y a aucun moyen de factoriser cette initialisation dans une autre fonction ( jusqu'à C ++ 0x, de toute façon modifier: C ++ a maintenant des constructeurs délégués )
  • la référence ne peut pas être rebondie ou être nulle. Cela peut être un avantage, mais si le code doit changer pour permettre la reliure ou pour que le membre soit nul, toutes les utilisations du membre doivent changer
  • contrairement aux membres pointeurs, les références ne peuvent pas être facilement remplacées par des pointeurs intelligents ou des itérateurs, car la refactorisation peut nécessiter
  • Chaque fois qu'une référence est utilisée, elle ressemble au type valeur ( .opérateur, etc.), mais se comporte comme un pointeur (peut pendre) - par exemple, Google Style Guide le décourage

66
Toutes les choses que vous avez mentionnées sont de bonnes choses à éviter, donc si les références aident à cela, elles sont bonnes et pas mauvaises. La liste d'initialisation est le meilleur endroit pour initier les données. Très souvent, vous devez masquer l'opérateur d'affectation, avec des références que vous n'avez pas à faire. "Ne peut pas être rebondi" - bien, la réutilisation des variables est une mauvaise idée.
Mykola Golubyev

4
@Mykola: Je suis d'accord avec vous. Je préfère que les membres soient initialisés dans les listes d'initialisation, j'évite les pointeurs nuls et ce n'est certainement pas bon pour une variable de changer sa signification. Là où nos opinions diffèrent, c'est que je n'ai pas besoin ou que je ne veux pas que le compilateur applique cela pour moi. Cela ne facilite pas l'écriture des classes, je n'ai pas rencontré de bogues dans ce domaine que cela pourrait attraper, et parfois j'apprécie la flexibilité que j'obtiens avec un code qui utilise systématiquement des membres pointeurs (pointeurs intelligents le cas échéant).
James Hopkin

Je pense que ne pas avoir à masquer l'affectation est un faux argument car vous n'avez pas à masquer l'affectation si la classe contient un pointeur qu'il n'est pas responsable de la suppression de toute façon - la valeur par défaut fera l'affaire. Je pense que c'est la meilleure réponse car elle donne de bonnes raisons pour lesquelles les pointeurs devraient être préférés aux références dans les données des membres.
markh44

4
James, ce n'est pas que cela rend le code plus facile à écrire, c'est que cela rend le code plus facile à lire. Avec une référence en tant que membre de données, vous n'avez jamais à vous demander si elle peut être nulle au point d'utilisation. Cela signifie que vous pouvez consulter le code avec moins de contexte requis.
Len Holgate le

4
Cela rend les tests unitaires et les simulations beaucoup plus difficiles, impossibles presque en fonction du nombre d'utilisateurs, combinés à une `` explosion de graphes d'affect '' sont à mon humble avis des raisons suffisamment convaincantes pour ne jamais les utiliser.
Chris Huang-Leaver le

156

Ma propre règle de base:

  • Utilisez un membre de référence lorsque vous voulez que la vie de votre objet dépende de la vie d'autres objets : c'est une façon explicite de dire que vous ne permettez pas à l'objet d'être vivant sans une instance valide d'une autre classe - à cause de non affectation et l'obligation d'obtenir l'initialisation des références via le constructeur. C'est un bon moyen de concevoir votre classe sans rien supposer que son instance soit membre ou non d'une autre classe. Vous supposez seulement que leur vie est directement liée à d'autres instances. Il vous permet de modifier ultérieurement la façon dont vous utilisez votre instance de classe (avec new, en tant qu'instance locale, en tant que membre de classe, généré par un pool de mémoire dans un gestionnaire, etc.)
  • Utilisez un pointeur dans les autres cas : lorsque vous souhaitez que le membre soit modifié ultérieurement, utilisez un pointeur ou un pointeur const pour être sûr de ne lire que l'instance pointée. Si ce type est censé être copiable, vous ne pouvez pas utiliser de références de toute façon. Parfois, vous devez également initialiser le membre après un appel de fonction spéciale (init () par exemple) et vous n'avez tout simplement pas d'autre choix que d'utiliser un pointeur. MAIS: utilisez des assertions dans toutes vos fonctions membres pour détecter rapidement le mauvais état du pointeur!
  • Dans les cas où vous souhaitez que la durée de vie de l'objet dépende de la durée de vie d' un objet externe, et que vous avez également besoin que ce type soit copiable, utilisez des membres de pointeur mais un argument de référence dans le constructeur De cette façon, vous indiquez lors de la construction que la durée de vie de cet objet dépend sur la durée de vie de l'argument MAIS l'implémentation utilise des pointeurs pour être toujours copiable. Tant que ces membres ne sont modifiés que par copie et que votre type n'a pas de constructeur par défaut, le type doit remplir les deux objectifs.

1
Je pense que la vôtre et celle de Mykola sont des réponses correctes et favorisent la programmation sans pointeur (lire moins de pointeur).
amit

37

Les objets devraient rarement permettre d'assigner et d'autres choses comme la comparaison. Si vous considérez un modèle d'entreprise avec des objets tels que «Service», «Employé», «Directeur», il est difficile d'imaginer un cas où un employé sera affecté à un autre.

Ainsi, pour les objets métier, il est très utile de décrire les relations un-à-un et un-à-plusieurs comme des références et non des pointeurs.

Et il est probablement correct de décrire la relation un ou zéro comme un pointeur.

Donc pas de facteur «nous ne pouvons pas attribuer».
Beaucoup de programmeurs s'habituent simplement aux pointeurs et c'est pourquoi ils trouveront n'importe quel argument pour éviter l'utilisation de référence.

Avoir un pointeur en tant que membre vous forcera, vous ou un membre de votre équipe, à vérifier le pointeur encore et encore avant utilisation, avec un commentaire "juste au cas où". Si un pointeur peut être nul, le pointeur est probablement utilisé comme une sorte d'indicateur, ce qui est mauvais, car chaque objet doit jouer son propre rôle.


2
+1: Même chose que j'ai vécu. Seules certaines classes de stockage de données génériques nécessitent assignemtn et copy-c'tor. Et pour les frameworks d'objets métier de plus haut niveau, il devrait y avoir d'autres techniques de copie qui gèrent également des choses comme "ne pas copier des champs uniques" et "ajouter à un autre parent", etc. Vous utilisez donc le framework de haut niveau pour copier vos objets métier et interdire les affectations de bas niveau.
mmmmmmmm

2
+1: Quelques points d'accord: les membres de référence empêchant la définition d'un opérateur d'affectation n'est pas un argument important. Comme vous indiquez correctement une manière différente, tous les objets n'ont pas de sémantique de valeur. Je conviens également que nous ne voulons pas de «si (p)» dispersé dans tout le code sans raison logique. Mais l'approche correcte consiste à utiliser les invariants de classe: une classe bien définie ne laissera aucun doute sur le fait que les membres peuvent être nuls. Si un pointeur peut être nul dans le code, je m'attendrais à ce que cela soit bien commenté.
James Hopkin

@JamesHopkin Je suis d'accord que des classes bien conçues peuvent utiliser efficacement des pointeurs. Outre la protection contre NULL, les références garantissent également que votre référence a été initialisée (un pointeur n'a pas besoin d'être initialisé, il peut donc pointer vers autre chose). Les références vous indiquent également la propriété - à savoir que l'objet ne possède pas le membre. Si vous utilisez systématiquement des références pour les membres agrégés, vous aurez un degré très élevé de confiance dans le fait que les membres pointeurs sont des objets composites (bien qu'il n'y ait pas beaucoup de cas où un membre composite est représenté par un pointeur).
weberc2


6

Dans quelques cas importants, la cessibilité n'est tout simplement pas nécessaire. Ce sont souvent des wrappers d'algorithmes légers qui facilitent le calcul sans quitter la portée. Ces objets sont des candidats de premier ordre pour les membres de référence, car vous pouvez être sûr qu'ils détiennent toujours une référence valide et n'ont jamais besoin d'être copiés.

Dans de tels cas, assurez-vous de rendre l'opérateur d'affectation (et souvent aussi le constructeur de copie) non utilisable (en héritant boost::noncopyableou en les déclarant privé).

Cependant, comme l'utilisateur pts l'a déjà commenté, il n'en va pas de même pour la plupart des autres objets. Ici, l'utilisation de membres de référence peut être un énorme problème et doit généralement être évitée.


6

Comme tout le monde semble donner des règles générales, j'en proposerai deux:

  • N'utilisez jamais, jamais de références d'utilisation en tant que membres de la classe. Je ne l'ai jamais fait dans mon propre code (sauf pour me prouver que j'avais raison dans cette règle) et je ne peux pas imaginer un cas où je le ferais. La sémantique est trop confuse, et ce n'est vraiment pas pour cela que les références ont été conçues.

  • Toujours, toujours, utilisez des références lors du passage de paramètres aux fonctions, sauf pour les types de base, ou lorsque l'algorithme nécessite une copie.

Ces règles sont simples et m'ont bien servi. Je laisse faire des règles sur l'utilisation des pointeurs intelligents (mais s'il vous plaît, pas auto_ptr) en tant que membres de la classe aux autres.


2
Ne soyez pas tout à fait d'accord sur le premier, mais le désaccord n'est pas un problème pour valoriser la justification. +1
David Rodríguez - dribeas

(Nous semblons perdre cet argument avec les bons électeurs de SO, cependant)
James Hopkin

2
J'ai voté contre parce que «N'utilisez jamais, jamais de références comme membres de la classe» et la justification suivante ne me convient pas. Comme expliqué dans ma réponse (et commenter la réponse principale) et d'après ma propre expérience, il y a des cas où c'est simplement une bonne chose. J'ai pensé comme vous jusqu'à ce que je sois embauché dans une entreprise où ils l'utilisaient de manière satisfaisante et cela m'a fait comprendre qu'il y a des cas où cela aide. En fait, je pense que le vrai problème est davantage la syntaxe et l'implication cachée qui font que les références sont utilisées car les membres exigent que le programmeur comprenne ce qu'il fait.
Klaim

1
Neil> Je suis d'accord mais ce n'est pas "l'opinion" qui m'a fait voter contre, c'est l'assertion "jamais". Comme argumenter ici ne semble pas être utile de toute façon, je vais annuler mon vote contre.
Klaim

2
Cette réponse pourrait être améliorée en expliquant en quoi la sémantique des membres de référence prête à confusion. Cette justification est importante car, à première vue, les pointeurs semblent plus sémantiquement ambigus (ils peuvent ne pas être initialisés et ils ne vous disent rien sur la propriété de l'objet sous-jacent).
weberc2

4

Oui à: Est-il vrai de dire qu'un objet contenant une référence ne doit pas être assignable, puisqu'une référence ne doit pas être modifiée une fois initialisée?

Mes règles de base pour les membres de données:

  • n'utilisez jamais de référence, car cela empêche l'affectation
  • si votre classe est responsable de la suppression, utilisez scoped_ptr de boost (qui est plus sûr qu'un auto_ptr)
  • sinon, utilisez un pointeur ou un pointeur const

2
Je ne peux même pas nommer un cas où j'aurais besoin d'une affectation de l'objet métier.
Mykola Golubyev

1
+1: J'ajouterais juste pour être prudent avec les membres auto_ptr - toute classe qui les a doit avoir une assignation et une construction de copie explicitement définies (les générées sont très peu susceptibles d'être ce qui est requis)
James Hopkin

auto_ptr empêche également l'affectation (sensible).

2
@Mykola: J'ai également fait l'expérience que 98% de mes objets métier n'ont pas besoin d'affectation ou de copie-c'tors. J'évite cela par une petite macro que j'ajoute aux en-têtes de ces classes ce qui rend la copie des c'tors et operator = () private sans implémentation. Donc, dans 98% des objets, j'utilise également des références et c'est sûr.
mmmmmmmm

1
Les références en tant que membres peuvent être utilisées pour déclarer explicitement que la vie de l'instance de la classe dépend directement de la vie des instances d'autres classes (ou la même chose). «N'utilisez jamais de référence, car cela empêche l'affectation» suppose que toutes les classes doivent autoriser l'affectation. C'est comme supposer que toutes les classes doivent autoriser l'opération de copie ...
Klaim

2

Je n'utiliserais généralement un pointeur dans les données des membres que lorsque le pointeur pourrait être nul ou changer. Y a-t-il d'autres raisons de préférer les pointeurs aux références pour les membres de données?

Oui. Lisibilité de votre code. Un pointeur rend plus évident que le membre est une référence (ironiquement :)), et non un objet contenu, car lorsque vous l'utilisez, vous devez le dé-référencer. Je sais que certaines personnes pensent que c'est démodé, mais je pense toujours que cela évite simplement la confusion et les erreurs.


1
Lakos soutient également cela dans "Large-Scale C ++ Software Design".
Johan Kotlinski

Je crois que Code Complete le préconise aussi et je ne suis pas contre. Ce n'est pas trop convaincant cependant ...
markh44

2

Je déconseille aux membres de données de référence parce que vous ne savez jamais qui va dériver de votre classe et ce qu'ils pourraient vouloir faire. Ils peuvent ne pas vouloir utiliser l'objet référencé, mais étant une référence, vous les avez forcés à fournir un objet valide. Je me suis suffisamment fait cela pour arrêter d'utiliser des membres de données de référence.


0

Si je comprends bien la question ...

Référence en tant que paramètre de fonction au lieu de pointeur: comme vous l'avez souligné, un pointeur n'indique pas clairement à qui appartient le nettoyage / l'initialisation du pointeur. Préférez un point partagé lorsque vous voulez un pointeur, c'est une partie de C ++ 11, et un low_ptr lorsque la validité des données n'est pas garantie pendant toute la durée de vie de la classe acceptant les données pointées. également une partie de C ++ 11. L'utilisation d'une référence comme paramètre de fonction garantit que la référence n'est pas nulle. Vous devez subvertir les fonctionnalités du langage pour contourner cela, et nous ne nous soucions pas des codeurs de canon lâches.

Référence une variable membre: comme ci-dessus en ce qui concerne la validité des données. Cela garantit que les données pointées, puis référencées, sont valides.

Le déplacement de la responsabilité de la validité de variable vers un point antérieur du code nettoie non seulement le code ultérieur (la classe A dans votre exemple), mais le rend également clair pour la personne qui utilise.

Dans votre exemple, qui est un peu déroutant (j'essaierais vraiment de trouver une implémentation plus claire), le A utilisé par B est garanti pour la durée de vie du B, puisque B est membre de A, donc une référence le renforce et est (peut-être) plus clair.

Et juste au cas où (ce qui est peu probable car cela n'aurait aucun sens dans le contexte de vos codes), une autre alternative, utiliser un paramètre A non référencé et non pointeur, copierait A et rendrait le paradigme inutile - je ne pensez vraiment pas que vous entendez cela comme une alternative.

En outre, cela garantit également que vous ne pouvez pas modifier les données référencées, où un pointeur peut être modifié. Un pointeur const ne fonctionnerait que si le pointeur référencé / vers les données n'était pas modifiable.

Les pointeurs sont utiles s'il n'est pas garanti que le paramètre A pour B soit défini ou s'il peut être réaffecté. Et parfois, un pointeur faible est tout simplement trop détaillé pour l'implémentation, et un bon nombre de personnes ne savent pas ce qu'est un low_ptr ou ne les aiment tout simplement pas.

Déchirez cette réponse, s'il vous plaît :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.