web-dev-qa-db-fra.com

Est-il préférable en C ++ de passer par valeur ou par référence constante?

Est-il préférable en C++ de passer par valeur ou par référence constante?

Je me demande quelle est la meilleure pratique. Je me rends compte que passer par référence constante devrait améliorer les performances du programme car vous ne faites pas de copie de la variable.

202
Matt Pascoe

C'était une pratique généralement recommandée1 to utilise la référence de passage par référence pour tous les types, sauf pour les types intégrés (char, int, double, etc.), pour les itérateurs et pour les objets fonction (lambdas, classes dérivées de std::*_function).

C'était particulièrement vrai avant l'existence de la sémantique de déplacement. La raison est simple: si vous passez par valeur, une copie de l'objet doit être faite et, à l'exception des très petits objets, cela coûte toujours plus cher que de passer une référence.

Avec C++ 11, nous avons gagné déplacer la sémantique . En résumé, la sémantique de déplacement permet, dans certains cas, de passer un objet "par valeur" sans le copier. C'est notamment le cas lorsque l'objet que vous passez est un rvalue .

En soi, déplacer un objet est toujours au moins aussi coûteux que de passer par référence. Cependant, dans de nombreux cas, une fonction copie de toute façon en interne un objet - c’est-à-dire qu’elle prendra propriété de l’argument.2

Dans ces situations, nous avons le compromis (simplifié) suivant:

  1. Nous pouvons passer l'objet par référence, puis copier en interne.
  2. Nous pouvons passer l'objet par valeur.

"Pass par valeur" provoque toujours la copie de l'objet, sauf s'il est une valeur rvalue. Dans le cas d'une valeur rvalue, l'objet peut être déplacé à la place, de sorte que le second cas ne soit plus du tout "copier, puis se déplacer", mais "se déplacer, puis se déplacer à nouveau".

Pour les grands objets qui implémentent les constructeurs de déplacement appropriés (tels que les vecteurs, les chaînes de caractères, etc.), le second cas est alors énormément plus efficace que le premier. Par conséquent, il est recommandé d'utiliser pass par valeur si la fonction devient propriétaire de l'argument et si le type d'objet prend en charge le déplacement efficace .


Une note historique:

En fait, tout compilateur moderne devrait pouvoir déterminer quand passer par valeur est coûteux et convertir implicitement l'appel pour utiliser une référence constante si possible.

En théorie. En pratique, les compilateurs ne peuvent pas toujours changer cela sans casser l’interface binaire de la fonction. Dans certains cas spéciaux (lorsque la fonction est en ligne), la copie sera supprimée si le compilateur peut déterminer que l’objet original ne sera pas modifié par les actions de la fonction.

Mais en général, le compilateur ne peut pas le déterminer et l’avènement de la sémantique de déplacement en C++ a rendu cette optimisation beaucoup moins pertinente.


1 Par exemple. dans Scott Meyers, Effective C++.

2 Ceci est particulièrement vrai pour les constructeurs d’objets, qui peuvent prendre des arguments et les stocker en interne pour faire partie de l’état de l’objet construit.

192
Konrad Rudolph

Edit: Nouvel article de Dave Abrahams sur cpp-next:

Vous voulez de la vitesse? Passe par la valeur.


Le passage par valeur pour les structures où la copie est peu coûteuse présente l'avantage supplémentaire que le compilateur peut supposer que les objets ne sont pas des alias (ne sont pas les mêmes objets). En utilisant passe par référence, le compilateur ne peut pas assumer cela toujours. Exemple simple:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

le compilateur peut l'optimiser dans

g.i = 15;
f->i = 2;

puisqu'il sait que f et g ne partagent pas le même emplacement. si g était une référence (foo &), le compilateur n'aurait pas pu supposer cela. puisque g.i pourrait alors être aliasé par f-> i et doit avoir une valeur de 7. le compilateur devrait donc extraire à nouveau la nouvelle valeur de g.i de la mémoire.

Pour des règles plus pratiques, voici un bon ensemble de règles trouvées dans Move Constructors article (lecture hautement recommandée).

  • Si la fonction a l'intention de changer l'argument en tant qu'effet secondaire, prenez-le par référence non-const.
  • Si la fonction ne modifie pas son argument et que l'argument est de type primitif, prenez-le par valeur.
  • Sinon, prenons-le par référence const, sauf dans les cas suivants
    • Si la fonction doit alors quand même faire une copie de la référence const, prenez-la par valeur.

"Primitive" signifie en gros de petits types de données d'une longueur de quelques octets, non polymorphes (itérateurs, objets fonction, etc.) ou coûteux à copier. Dans ce document, il y a une autre règle. L'idée est que parfois on veut faire une copie (au cas où l'argument ne peut pas être modifié) et parfois on ne veut pas (au cas où on veut utiliser l'argument lui-même dans la fonction si l'argument était de toute façon temporaire , par exemple). Le document explique en détail comment cela peut être fait. En C++ 1x, cette technique peut être utilisée de manière native avec le support de la langue. Jusque-là, j'irais avec les règles ci-dessus.

Exemples: Pour faire une chaîne en majuscule et renvoyer la version en majuscule, il faut toujours passer par valeur: il faut quand même en prendre une copie (on ne peut pas changer directement la référence const) - il vaut donc mieux la rendre aussi transparente que possible. l’appelant et effectuez cette copie à l’avance afin que l’appelant puisse optimiser autant que possible - comme indiqué dans ce document:

my::string uppercase(my::string s) { /* change s and return it */ }

Cependant, si vous n'avez pas besoin de changer le paramètre, prenez-le en référence à const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Cependant, si le paramètre a pour but d'écrire quelque chose dans l'argument, transmettez-le par référence non const.

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
96

Dépend du type. Vous ajoutez la petite surcharge de devoir faire une référence et déréférence. Pour les types de taille égale ou inférieure aux pointeurs utilisant le copieur par défaut, il serait probablement plus rapide de passer par valeur.

12
Lou Franco

Comme il a été souligné, cela dépend du type. Pour les types de données intégrés, il est préférable de passer par valeur. Même de très petites structures, telles qu'une paire d'ints, peuvent être plus performantes en passant par valeur.

Voici un exemple, supposons que vous avez une valeur entière et que vous voulez la transmettre à une autre routine. Si cette valeur a été optimisée pour être stockée dans un registre, si vous voulez la transmettre comme référence, elle doit d’abord être stockée en mémoire, puis un pointeur sur cette mémoire placée sur la pile pour effectuer l’appel. S'il était passé par valeur, il suffit que le registre soit placé dans la pile. (Les détails sont un peu plus compliqués que ceux donnés par différents systèmes d’appel et processeurs).

Si vous faites de la programmation par modèle, vous êtes généralement obligé de toujours passer par référence constante car vous ne connaissez pas les types transmis. Passer des pénalités pour avoir passé quelque chose de mauvais par valeur est bien pire que de passer par un type intégré par réf const.

8
Torlack

C’est ce que je fais normalement lors de la conception de l’interface d’une fonction non-template:

  1. Passer par valeur si la fonction ne veut pas modifier le paramètre et que la valeur est peu coûteuse à copier (int, double, float, char, bool, etc ... Notez que std :: string, std :: vector et le reste des conteneurs dans la bibliothèque standard ne sont pas)

  2. Passer par le pointeur const si la valeur est chère à copier et que la fonction ne souhaite pas modifier la valeur pointée et que NULL est une valeur que la fonction gère.

  3. Passez par le pointeur non-const si la valeur est chère à copier et que la fonction veut modifier la valeur pointée et que NULL est une valeur que la fonction gère.

  4. Passer en référence const lorsque la valeur est chère à copier et que la fonction ne veut pas modifier la valeur à laquelle il est fait référence, et NULL ne serait pas une valeur valide si un pointeur était utilisé à la place.

  5. Passer par référence non-const lorsque la valeur est chère à copier et que la fonction veut modifier la valeur à laquelle il est fait référence, et NULL ne serait pas une valeur valide si un pointeur était utilisé à la place.

6
Martin G

On dirait que tu as eu ta réponse. Passer par valeur coûte cher, mais vous en donne une copie si vous en avez besoin.

5
GeekyMonkey

En règle générale, mieux vaut passer par référence constante. Mais si vous avez besoin de modifier votre argument de fonction localement, vous devriez mieux utiliser le passage par valeur. Pour certains types de base, les performances sont généralement les mêmes pour le passage par valeur et par référence. En fait, référence référencée en interne représentée par un pointeur, c’est pourquoi on peut s’attendre, par exemple, à ce que les points de passage soient identiques en termes de performances ou que le passage par valeur soit plus rapide en raison d’un déréférencement inutile.

4
sergtk

En règle générale, valeur pour les types non-classes et référence const pour les classes. Si une classe est vraiment petite, il vaut probablement mieux passer par valeur, mais la différence est minime. Ce que vous voulez vraiment éviter, c’est de passer par une classe gigantesque par valeur et de tout dupliquer - cela fera une énorme différence si vous passez, par exemple, un std :: vector avec pas mal d’éléments.

1
Peter

Pass par valeur pour les petits types.

Passer les références const pour les gros types (la définition de gros peut varier selon les machines) MAIS, en C++ 11, passe par valeur si vous allez utiliser les données, car vous pouvez exploiter la sémantique du déplacement. Par exemple:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Maintenant, le code d'appel ferait:

Person p(std::string("Albert"));

Et un seul objet serait créé et déplacé directement dans le membre name_ en classe Person. Si vous passez par référence const, il faudra en créer une copie pour la mettre dans name_.

1
Germán Diago