238 votes

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

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

Je me demande quelle est la meilleure pratique. Je sais que passer par référence-constante devrait offrir de meilleures performances dans le programme car vous ne faites pas de copie de la variable.

0 votes

236voto

Konrad Rudolph Points 231505

Il était autrefois généralement recommandé1 de utiliser pass by const ref pour tous les types, sauf pour les types primitifs (char, int, double, etc.), pour les itérateurs et pour les objets fonctionnels (lambdas, classes dérivant de std::*_function).

C'était particulièrement vrai avant l'existence des sémantiques de déplacement. La raison est simple: si vous passiez par valeur, une copie de l'objet devait être faite et, sauf pour les objets très petits, cela est toujours plus coûteux que de passer une référence.

Avec C++11, nous avons gagné les sémantiques de déplacement. En un mot, les sémantiques de déplacement permettent que, dans certains cas, un objet peut être passé "par valeur" sans le copier. En particulier, c'est 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 passer par référence. Cependant, dans de nombreux cas, une fonction copiera de toute façon un objet internement - c'est-à-dire qu'elle prendra la possession 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 internement.
  2. Nous pouvons passer l'objet par valeur.

"Passer par valeur" cause toujours la copie de l'objet, sauf si l'objet est un rvalue. Dans le cas d'un rvalue, l'objet peut être déplacé au lieu d'être copié, de sorte que le deuxième cas n'est soudainement plus "copier, puis déplacer" mais "déplacer, puis (potentiellement) déplacer à nouveau".

Pour les grands objets qui implémentent des constructeurs de déplacement adéquats (comme des vecteurs, des chaînes de caractères ...), le deuxième cas est alors nettement plus efficace que le premier. Par conséquent, il est recommandé de utiliser le passage par valeur si la fonction prend la possession de l'argument, et si le type d'objet prend en charge un déplacement efficace.


Une note historique:

En fait, n'importe quel compilateur moderne devrait être capable de comprendre quand le passage par valeur est coûteux, et convertir implicitement l'appel pour utiliser une const ref si possible.

En théorie. En pratique, les compilateurs ne peuvent pas toujours changer cela sans rompre l'interface binaire de la fonction. Dans certains cas spéciaux (lorsque la fonction est inlinée), la copie sera en fait éliminée si le compilateur peut comprendre que l'objet original ne sera pas modifié par les actions dans la fonction.

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


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

2 Cela est particulièrement souvent vrai pour les constructeurs d'objets, qui peuvent prendre des arguments et les stocker internement pour faire partie de l'état de l'objet construit.

0 votes

Hmmm ... Je ne suis pas sûr que cela vaut la peine de passer par référence. double-s

4 votes

Comme d'habitude, boost aide ici. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm contient du matériel de modèle pour déterminer automatiquement quand un type est un type intégré (utile pour les modèles, où parfois vous ne pouvez pas le savoir facilement).

18 votes

Cette réponse manque un point important. Pour éviter le slicing, vous devez passer par référence (const ou autre). Voir stackoverflow.com/questions/274626/…

104voto

Éditer : Nouvel article de Dave Abrahams sur cpp-next:

Voulez-vous de la vitesse ? Passez par valeur.


Passer par valeur pour les structures où la copie est bon marché a 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 passant par référence, le compilateur ne peut pas toujours le supposer. Exemple simple :

foo * f;

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

le compilateur peut l'optimiser en

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

car il sait que f et g ne partagent pas le même emplacement. si g était une référence (foo &), le compilateur ne pourrait pas l'assumer. car g.i pourrait alors être un alias de f->i et devrait avoir une valeur de 7. donc le compilateur devrait récupérer la nouvelle valeur de g.i depuis la mémoire.

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

  • Si la fonction a l'intention de modifier l'argument comme effet secondaire, prenez-le par référence non constante.
  • Si la fonction ne modifie pas son argument et que l'argument est de type primitif, prenez-le par valeur.
  • Sinon, prenez-le par référence constante, sauf dans les cas suivants
    • Si la fonction doit alors faire une copie de la référence constante de toute façon, prenez-la par valeur.

"Primitive" ci-dessus signifie essentiellement des types de données petits qui sont de quelques octets de longueur et ne sont pas polymorphiques (itérateurs, objets de fonction, etc...) ou coûteux à copier. Dans cet article, il y a une autre règle. L'idée est que parfois on veut faire une copie (au cas où l'argument ne pourrait pas être modifié), et parfois on ne veut pas (au cas où on voudrait utiliser l'argument lui-même dans la fonction si l'argument était de toute façon temporaire, par exemple). L'article explique en détail comment cela peut être fait. En C++1x, cette technique peut être utilisée nativement avec le support du langage. Jusque-là, je suivrais les règles ci-dessus.

Exemples : Pour mettre une chaîne en majuscule et retourner la version en majuscule, il faut toujours passer par valeur : il faut de toute façon en faire une copie (on ne pourrait pas modifier la référence constante directement) - donc autant la rendre aussi transparente que possible pour l'appelant et faire cette copie tôt pour qu'il puisse optimiser autant que possible - comme détaillé dans cet article:

my::string majuscule(my::string s) { /* changer s et le retourner */ }

Cependant, si vous n'avez pas besoin de changer le paramètre de toute façon, passez par référence constante :

bool tout_majuscule(my::string const& s) { 
    /* vérifier si un caractère est en majuscule */
}

Cependant, si le but du paramètre est d'écrire quelque chose dans l'argument, passez-le par référence non constante

bool essayer_parse(T texte, my::string &out) {
    /* essayer de parser, écrire le résultat dans out */
}

0 votes

J'ai trouvé vos règles bonnes mais je ne suis pas sûr de la première partie où vous parlez du fait de ne pas le passer en tant que référence accélérerait. Ouais, c'est sûr, mais ne pas passer quelque chose en tant que référence juste pour des raisons d'optimisation n'a aucun sens du tout. Si vous voulez modifier l'objet de pile que vous passez, faites-le par référence. Si vous ne le faites pas, passez-le par valeur. Si vous ne voulez pas le modifier, passez-le en tant que const-référence. L'optimisation liée au passage par valeur ne devrait pas avoir d'importance car vous gagnez d'autres choses en passant par référence. Je ne comprends pas le "voulez-vous de la vitesse?" car si vous alliez effectuer ces opérations, vous passeriez de toute façon par valeur.

0 votes

Johannes : J'ai adoré cet article quand je l'ai lu, mais j'ai été déçu quand j'ai essayé. Ce code a échoué à la fois sur GCC et MSVC. Ai-je manqué quelque chose, ou cela ne fonctionne-t-il pas en pratique ?

0 votes

Je ne pense pas être d'accord que si vous voulez toujours faire une copie, vous la passez par valeur (au lieu de const ref), puis la déplacez. Regardez les choses de cette manière, qu'est-ce qui est plus efficace, une copie et un déplacement (vous pouvez même avoir 2 copies si vous la passez en avant), ou simplement une copie? Oui, il y a quelques cas spéciaux de chaque côté, mais si vos données ne peuvent de toute façon pas être déplacées (par exemple : un POD avec des tonnes d'entiers), pas besoin de copies supplémentaires.

16voto

Lou Franco Points 48823

Cela dépend du type. Vous ajoutez le léger surcoût de devoir faire une référence et une déréférence. Pour les types dont la taille est inférieure ou égale à celle des pointeurs qui utilisent le constructeur de copie par défaut, il serait probablement plus rapide de passer par valeur.

2 votes

Pour les types non natifs, vous pourriez (selon l'efficacité du compilateur en optimisant le code) obtenir une augmentation des performances en utilisant des références constantes au lieu de simples références.

9voto

Torlack Points 2910

Comme cela 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'entiers, peuvent fonctionner mieux en passant par valeur.

Voici un exemple, supposons que vous ayez une valeur entière et que vous vouliez la passer à une autre routine. Si cette valeur a été optimisée pour être stockée dans un registre, alors si vous voulez la passer par référence, elle doit d'abord être stockée en mémoire, puis un pointeur pointant vers cette mémoire est placé sur la pile pour effectuer l'appel. Si elle était passée par valeur, il suffirait de pousser le registre sur la pile. (Les détails sont un peu plus compliqués que cela étant donné les différents systèmes d'appel et processeurs).

Si vous faites de la programmation en modèle, vous êtes généralement obligé de toujours passer par const ref car vous ne connaissez pas les types passés. Les pénalités pour passer quelque chose de mauvais par valeur sont bien pires que les pénalités de passer un type intégré par const ref.

0 votes

Note sur la terminologie: une structure contenant un million d'entiers est toujours un "type POD". Vous voulez peut-être dire 'pour les types intégrés, il est préférable de passer par valeur'.

5voto

GeekyMonkey Points 5036

On dirait que vous avez obtenu votre réponse. Passer par valeur est coûteux, mais vous donne une copie avec laquelle travailler si vous en avez besoin.

0 votes

Je ne suis pas sûr pourquoi cela a été voté à la baisse ? Cela me semble logique. Si vous avez besoin de la valeur actuellement stockée, passez par valeur. Sinon, passez par référence.

5 votes

C'est totalement dépendant du type. Faire un type POD (données simples) par référence peut en fait réduire les performances en provoquant plus d'accès mémoire.

1 votes

Évidemment, passer un int par référence ne sauve rien! Je pense que la question implique des choses plus grandes qu'un pointeur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X