Y a-t-il des avantages à passer par un pointeur sur le passage par référence en C ++?

225

Quels sont les avantages du passage par pointeur par rapport au passage par référence en C ++?

Dernièrement, j'ai vu un certain nombre d'exemples qui ont choisi de passer des arguments de fonction par des pointeurs au lieu de passer par référence. Y a-t-il des avantages à faire cela?

Exemple:

func(SPRITE *x);

avec un appel de

func(&mySprite);

contre.

func(SPRITE &x);

avec un appel de

func(mySprite);
Matt Pascoe
la source
N'oubliez pas newde créer un pointeur et les problèmes de propriété qui en résultent.
Martin York

Réponses:

216

Un pointeur peut recevoir un paramètre NULL, pas un paramètre de référence. S'il y a une chance que vous souhaitiez passer "aucun objet", utilisez un pointeur au lieu d'une référence.

De plus, le passage par un pointeur vous permet de voir explicitement sur le site d'appel si l'objet est passé par valeur ou par référence:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);
Adam Rosenfield
la source
18
Réponse incomplète. L'utilisation de pointeurs n'autorise pas l'utilisation d'objets temporaires / promus, ni l'utilisation d'objets pointés comme des objets de type pile. Et cela suggérera que l'argument peut être NULL quand, la plupart du temps, une valeur NULL devrait être interdite. Lisez la réponse de litb pour une réponse complète.
paercebal
Le deuxième appel de fonction était annoté func2 passes by reference. Bien que j'apprécie que vous vouliez dire qu'il passe "par référence" dans une perspective de haut niveau, implémenté en passant un pointeur dans une perspective au niveau du code, cela était très déroutant (voir stackoverflow.com/questions/13382356/… ).
Courses de légèreté en orbite
Je n'achète pas ça. Oui, vous passez un pointeur, donc ce doit être un paramètre de sortie, car ce qui est indiqué ne peut pas être const?
deworde
N'avons-nous pas cette référence qui passe en C? J'utilise la dernière version de codeblock (mingw) et je sélectionne un projet C. Toujours en passant par référence (func (int & a)) fonctionne. Ou est-il disponible à partir de C99 ou C11?
Jon Wheelock
1
@JonWheelock: Non, C n'a pas du tout de référence. func(int& a)n'est pas C valide dans aucune version de la norme. Vous compilez probablement vos fichiers en C ++ par accident.
Adam Rosenfield du
245

En passant par le pointeur

  • L'appelant doit prendre l'adresse -> pas transparent
  • Une valeur 0 peut être fournie pour signifier nothing. Cela peut être utilisé pour fournir des arguments facultatifs.

Passer par référence

  • L'appelant passe simplement l'objet -> transparent. Doit être utilisé pour la surcharge de l'opérateur, car la surcharge pour les types de pointeurs n'est pas possible (les pointeurs sont des types intégrés). Vous ne pouvez donc pas string s = &str1 + &str2;utiliser de pointeurs.
  • Aucune valeur 0 possible -> La fonction appelée n'a pas à les vérifier
  • La référence à const accepte également les temporaires: les void f(const T& t); ... f(T(a, b, c));pointeurs ne peuvent pas être utilisés comme ça car vous ne pouvez pas prendre l'adresse d'un temporaire.
  • Enfin, les références sont plus faciles à utiliser -> moins de risques de bugs.
Johannes Schaub - litb
la source
7
Passer par un pointeur soulève également la question «La propriété est-elle transférée ou non? question. Ce n'est pas le cas des références.
Frerich Raabe
43
Je ne suis pas d'accord avec "moins de risques de bugs". Lors de l'inspection du site d'appel et le lecteur voit "foo (& s)", il est immédiatement clair que s peut être modifié. Lorsque vous lisez "foo (s)", il n'est pas du tout clair si s peut être modifié. Ceci est une source majeure de bugs. Il y a peut-être moins de chance d'une certaine classe de bogues, mais dans l'ensemble, le passage par référence est une énorme source de bogues.
William Pursell
25
Qu'entendez-vous par "transparent"?
Gbert90
2
@ Gbert90, si vous voyez foo (& a) sur un site d'appel, vous savez que foo () prend un type de pointeur. Si vous voyez foo (a), vous ne savez pas s'il faut une référence.
Michael J. Davenport,
3
@ MichaelJ.Davenport - dans votre explication, vous suggérez "transparent" pour signifier quelque chose comme "évident que l'appelant passe un pointeur, mais pas évident que l'appelant passe une référence". Dans le message de Johannes, il dit "Passer par un pointeur - L'appelant doit prendre l'adresse -> pas transparent" et "Passer par référence - L'appelant passe juste l'objet -> transparent" - ce qui est presque opposé à ce que vous dites . Je pense que la question de Gbert90 "Que voulez-vous dire par" transparent "" est toujours valable.
Happy Green Kid Naps
66

J'aime le raisonnement d'un article de "cplusplus.com:"

  1. Passez par valeur lorsque la fonction ne veut pas modifier le paramètre et que la valeur est facile à copier (ints, doubles, char, bool, etc ... types simples. Std :: string, std :: vector et tous les autres STL les conteneurs ne sont PAS des types simples.)

  2. Passez par le pointeur const lorsque la valeur est coûteuse à copier ET la fonction ne veut pas modifier la valeur pointée sur ET NULL est une valeur attendue valide que la fonction gère.

  3. Passez un pointeur non const lorsque la valeur est coûteuse à copier ET que la fonction souhaite modifier la valeur pointée sur ET NULL est une valeur attendue valide que la fonction gère.

  4. Passez par référence const lorsque la valeur est coûteuse à copier ET la fonction ne veut pas modifier la valeur référencée ET NULL ne serait pas une valeur valide si un pointeur était utilisé à la place.

  5. Passer une référence non cont lorsque la valeur est coûteuse à copier ET la fonction veut modifier la valeur référencée ET NULL ne serait pas une valeur valide si un pointeur était utilisé à la place.

  6. Lors de l'écriture de fonctions de modèle, il n'y a pas de réponse claire car il y a quelques compromis à considérer qui dépassent le cadre de cette discussion, mais il suffit de dire que la plupart des fonctions de modèle prennent leurs paramètres par valeur ou référence (const) , cependant, car la syntaxe de l'itérateur est similaire à celle des pointeurs (astérisque pour "déréférencer"), toute fonction de modèle qui attend des itérateurs comme arguments acceptera également par défaut les pointeurs (et ne vérifiera pas NULL car le concept d'itérateur NULL a une syntaxe différente ).

http://www.cplusplus.com/articles/z6vU7k9E/

Ce que je retiens de cela, c'est que la principale différence entre le choix d'utiliser un pointeur ou un paramètre de référence est que NULL est une valeur acceptable. C'est tout.

Que la valeur soit entrée, sortie, modifiable, etc. devrait être dans la documentation / commentaires sur la fonction, après tout.

R. Navega
la source
Oui, pour moi, les termes liés à NULL sont les principales préoccupations ici. Merci de citer ..
binaryguy
62

«Enough Rope to Shoot Yourself in the Foot» d'Allen Holub énumère les 2 règles suivantes:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Il énumère plusieurs raisons pour lesquelles des références ont été ajoutées à C ++:

  • ils sont nécessaires pour définir les constructeurs de copie
  • ils sont nécessaires pour les surcharges de l'opérateur
  • const les références vous permettent d'avoir une sémantique passe-par-valeur tout en évitant une copie

Son principal argument est que les références ne doivent pas être utilisées comme paramètres de «sortie» car, sur le site de l'appel, rien n'indique si le paramètre est une référence ou un paramètre de valeur. Donc, sa règle est de n'utiliser que des constréférences comme arguments.

Personnellement, je pense que c'est une bonne règle de base car cela rend plus clair quand un paramètre est un paramètre de sortie ou non. Cependant, bien que je sois personnellement d'accord avec cela en général, je me laisse influencer par les opinions des autres membres de mon équipe s'ils plaident pour des paramètres de sortie comme références (certains développeurs les aiment énormément).

Michael Burr
la source
8
Ma position dans cet argument est que si le nom de la fonction rend totalement évident, sans vérifier les documents, que le paramètre sera modifié, alors une référence non const est OK. Donc, personnellement, j'autoriserais "getDetails (DetailStruct & result)". Un pointeur là soulève la possibilité laide d'une entrée NULL.
Steve Jessop
3
C'est trompeur. Même si certains n'aiment pas les références, elles sont une partie importante du langage et devraient être utilisées comme cela. Ce raisonnement revient à dire: n'utilisez pas de modèles, vous pouvez toujours utiliser des conteneurs de void * pour stocker n'importe quel type. Lisez la réponse de litb.
David Rodríguez - dribeas
4
Je ne vois pas en quoi cela est trompeur - il y a des moments où des références sont requises, et il y a des moments où les meilleures pratiques peuvent suggérer de ne pas les utiliser même si vous le pouvez. La même chose peut être dite pour n'importe quelle caractéristique du langage - héritage, amis non membres, surcharge d'opérateur, MI, etc ...
Michael Burr
Soit dit en passant, je conviens que la réponse de litb est très bonne, et est certainement plus complète que celle-ci - j'ai simplement choisi de me concentrer sur la discussion d'une justification pour éviter d'utiliser des références comme paramètres de sortie.
Michael Burr
1
Cette règle est utilisée dans le guide de style google c ++: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko
9

Précisions sur les postes précédents:


Les références ne garantissent PAS l' obtention d'un pointeur non nul. (Bien que nous les traitions souvent comme tels.)

Bien que le code soit horriblement mauvais, comme pour vous faire sortir du mauvais code de Woodshed , ce qui suit se compilera et s'exécutera: (Au moins sous mon compilateur.)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

Le vrai problème que j'ai avec les références réside dans d'autres programmeurs, désormais appelés IDIOTS , qui allouent dans le constructeur, désallouent dans le destructeur et ne fournissent pas de constructeur de copie ou d'opérateur = ().

Soudain, il y a un monde de différence entre foo (BAR bar) et foo (BAR & bar) . (L'opération de copie automatique au niveau du bit est invoquée. La désallocation dans le destructeur est invoquée deux fois.)

Heureusement, les compilateurs modernes prendront cette double désallocation du même pointeur. Il y a 15 ans, ils ne l'ont pas fait. (Sous gcc / g ++, utilisez setenv MALLOC_CHECK_ 0 pour revoir les anciennes méthodes.) Il en résulte, sous DEC UNIX, que la même mémoire est allouée à deux objets différents. Beaucoup de débogage amusant là-bas ...


Plus concrètement:

  • Les références cachent que vous modifiez des données stockées ailleurs.
  • Il est facile de confondre une référence avec un objet copié.
  • Les pointeurs le rendent évident!
Mr.Ree
la source
16
ce n'est pas le problème de la fonction ou des références. vous enfreignez les règles linguistiques. déréférencer un pointeur nul par lui-même est déjà un comportement non défini. "Les références ne sont PAS une garantie d'obtenir un pointeur non nul.": La norme elle-même le dit. d'autres façons constituent un comportement indéfini.
Johannes Schaub - litb
1
Je suis d'accord avec litb. Bien que cela soit vrai, le code que vous nous montrez est plus un sabotage qu'autre chose. Il existe des moyens de saboter quoi que ce soit, y compris les notations "référence" et "pointeur".
paercebal
1
J'ai dit que c'était "vous emmener derrière le mauvais code du bûcher"! Dans la même veine, vous pouvez également avoir i = new FOO; supprimer i; test (* i); Une autre occurrence de pointeur / référence pendante (malheureusement courante).
Mr.Ree
1
Ce n'est en fait pas le déréférencement de NULL qui est le problème, mais plutôt L' UTILISATION de cet objet déréférencé (null). En tant que tel, il n'y a vraiment aucune différence (autre que la syntaxe) entre les pointeurs et les références du point de vue de l'implémentation du langage. Ce sont les utilisateurs qui ont des attentes différentes.
Mr.Ree
2
Peu importe ce que vous faites avec la référence renvoyée, au moment où vous dites *i, votre programme a un comportement indéfini. Par exemple, le compilateur peut voir ce code et supposer "OK, ce code a un comportement indéfini dans tous les chemins de code, donc toute cette fonction doit être inaccessible." Ensuite, il supposera que toutes les branches menant à cette fonction ne sont pas prises. Il s'agit d'une optimisation régulièrement effectuée.
David Stone
5

La plupart des réponses ici ne parviennent pas à résoudre l'ambiguïté inhérente à la présence d'un pointeur brut dans une signature de fonction, en termes d'expression de l'intention. Les problèmes sont les suivants:

  • L'appelant ne sait pas si le pointeur pointe vers un seul objet ou vers le début d'un "tableau" d'objets.

  • L'appelant ne sait pas si le pointeur "possède" la mémoire vers laquelle il pointe. IE, que la fonction libère ou non la mémoire. ( foo(new int)- Est-ce une fuite de mémoire?).

  • L'appelant ne sait pas s'il nullptrpeut ou non être transmis en toute sécurité à la fonction.

Tous ces problèmes sont résolus par des références:

  • Les références se réfèrent toujours à un seul objet.

  • Les références ne possèdent jamais la mémoire à laquelle elles se réfèrent, elles ne sont qu'une vue dans la mémoire.

  • Les références ne peuvent pas être nulles.

Cela fait des références un bien meilleur candidat pour une utilisation générale. Cependant, les références ne sont pas parfaites - il y a quelques problèmes majeurs à considérer.

  • Pas d'indirection explicite. Ce n'est pas un problème avec un pointeur brut, car nous devons utiliser l' &opérateur pour montrer que nous passons bien un pointeur. Par exemple, int a = 5; foo(a);il n'est pas du tout clair ici que a est passé par référence et pourrait être modifié.
  • Nullabilité. Cette faiblesse des pointeurs peut également être une force, lorsque nous voulons réellement que nos références soient annulables. Étant donné que ce std::optional<T&>n'est pas valide (pour de bonnes raisons), les pointeurs nous donnent la nullité que vous souhaitez.

Il semble donc que lorsque nous voulons une référence nullable avec une indirection explicite, nous devrions atteindre un T*droit? Faux!

Abstractions

Dans notre désespoir de nullité, nous pouvons atteindre T*et simplement ignorer toutes les lacunes et l'ambiguïté sémantique énumérées précédemment. Au lieu de cela, nous devrions rechercher ce que C ++ fait le mieux: une abstraction. Si nous écrivons simplement une classe qui entoure un pointeur, nous gagnons l'expressivité, ainsi que la nullité et l'indirection explicite.

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

C'est l'interface la plus simple que j'ai pu trouver, mais elle fait le travail efficacement. Il permet d'initialiser la référence, de vérifier si une valeur existe et d'accéder à la valeur. On peut l'utiliser comme ça:

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

Nous avons atteint nos objectifs! Voyons maintenant les avantages, par rapport au pointeur brut.

  • L'interface montre clairement que la référence ne doit faire référence qu'à un seul objet.
  • De toute évidence, il ne possède pas la mémoire à laquelle il se réfère, car il n'a pas de destructeur défini par l'utilisateur et aucune méthode pour supprimer la mémoire.
  • L'appelant sait qu'il nullptrpeut être transmis, car l'auteur de la fonction demande explicitement unoptional_ref

Nous pourrions rendre l'interface plus complexe à partir d'ici, comme l'ajout d'opérateurs d'égalité, une interface monadique get_oret map, une méthode qui obtient la valeur ou lève une exception,constexpr support. Vous pouvez le faire.

En conclusion, au lieu d'utiliser des pointeurs bruts, expliquez ce que ces pointeurs signifient réellement dans votre code, et tirez parti d'une abstraction de bibliothèque standard ou écrivez la vôtre. Cela améliorera considérablement votre code.

Tom VH
la source
3

Pas vraiment. En interne, le passage par référence est effectué en passant essentiellement l'adresse de l'objet référencé. Donc, il n'y a vraiment aucun gain d'efficacité à faire en passant un pointeur.

Passer par référence présente cependant un avantage. Vous êtes assuré d'avoir une instance de n'importe quel objet / type transmis. Si vous passez un pointeur, vous courez le risque de recevoir un pointeur NULL. En utilisant pass-by-reference, vous poussez un contrôle NULL implicite d'un niveau vers l'appelant de votre fonction.

Brian
la source
1
C'est à la fois un avantage et un inconvénient. De nombreuses API utilisent des pointeurs NULL pour signifier quelque chose d'utile (c'est-à-dire que NULL timepec attend indéfiniment, tandis que la valeur signifie attendre aussi longtemps).
Greg Rogers
1
@Brian: Je ne veux pas être complice mais: je ne dirais pas que l'on est assuré d'obtenir une instance lors de l'obtention d'une référence. Les références pendantes sont toujours possibles si l'appelant d'une fonction annule la référence à un pointeur pendant que l'appelé ne peut pas connaître.
foraidt
parfois, vous pouvez même gagner en performances en utilisant des références, car elles n'ont pas besoin de prendre de stockage et aucune adresse ne leur est affectée. aucune indirection requise.
Johannes Schaub - litb
Les programmes qui contiennent des références pendantes ne sont pas des C ++ valides. Par conséquent, oui, le code peut supposer que toutes les références sont valides.
Konrad Rudolph
2
Je peux définitivement déréférencer un pointeur nul et le compilateur ne pourra pas le dire ... si le compilateur ne peut pas dire que c'est "C ++ invalide", est-il vraiment invalide?
rmeador