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

213

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 me rends compte que le passage par référence constante devrait permettre de meilleures performances dans le programme car vous ne faites pas de copie de la variable.

Matt Pascoe
la source

Réponses:

203

Il a utilisé la meilleure pratique généralement recommandée 1 à l'utilisation passer par ref const pour tous les types , à l' exception des types (builtin char, int, double, etc.), pour itérateurs et des objets de fonction (lambdas, classes dérivées de std::*_function).

Cela était particulièrement vrai avant l'existence de la sémantique des mouvements . La raison est simple: si vous passez par valeur, une copie de l'objet doit être faite et, sauf pour les très petits objets, c'est toujours plus cher que de passer une référence.

Avec C ++ 11, nous avons acquis une sémantique de mouvement . En un mot, la sémantique de mouvement permet que, dans certains cas, un objet puisse être passé «par valeur» sans le copier. C'est notamment le cas lorsque l'objet que vous passez est une valeur r .

En soi, déplacer un objet est toujours au moins aussi cher que passer par référence. Cependant, dans de nombreux cas, une fonction copiera de toute façon un objet en interne - c'est-à-dire qu'elle s'appropriera 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. On peut passer l'objet par valeur.

"Passer par valeur" entraîne toujours la copie de l'objet, sauf si l'objet est une valeur r. Dans le cas d'une valeur r, l'objet peut être déplacé à la place, de sorte que le deuxième cas n'est plus soudainement «copier, puis déplacer» mais «déplacer, puis (potentiellement) se déplacer à nouveau».

Pour les gros objets qui implémentent des constructeurs de déplacement appropriés (tels que des vecteurs, des chaînes…), le deuxième cas est alors beaucoup plus efficace que le premier. Par conséquent, il est recommandé d' utiliser la fonction passer par valeur si la fonction prend possession de l'argument et si le type d'objet prend en charge un déplacement efficace .


Une note historique:

En fait, tout compilateur moderne devrait être capable de comprendre quand le passage par valeur est coûteux, et convertir implicitement l'appel pour utiliser une référence const 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 réellement éluée si le compilateur peut comprendre que l'objet d'origine ne sera pas modifié par les actions de la fonction.

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


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

2 Cela 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.

Konrad Rudolph
la source
hmmm ... Je ne suis pas sûr que cela vaille la peine de passer par ref. double-s
sergtk
3
Comme d'habitude, boost aide ici. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm a des trucs de modèle pour déterminer automatiquement quand un type est un type intégré (utile pour les modèles, où vous ne pouvez parfois pas le savoir facilement).
CesarB
13
Cette réponse manque un point important. Pour éviter le découpage, vous devez passer par référence (const ou autre). Voir stackoverflow.com/questions/274626/…
ChrisN
6
@Chris: à droite. J'ai laissé de côté toute la partie du polymorphisme parce que c'est une sémantique complètement différente. Je crois que l'OP (sémantiquement) signifiait le passage de l'argument «par valeur». Lorsque d'autres sémantiques sont nécessaires, la question ne se pose même pas.
Konrad Rudolph
98

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

Envie de vitesse? Passez par valeur.


Passer la 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 alias (ne sont pas les mêmes objets). En utilisant pass-by-reference, le compilateur ne peut pas toujours supposer cela. 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 n'aurait pas pu supposer cela. puisque gi pourrait alors être aliasé par f-> i et avoir une valeur de 7. donc le compilateur devrait récupérer à nouveau la nouvelle valeur de gi dans la mémoire.

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

  • Si la fonction a l'intention de changer 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 const, sauf dans les cas suivants
    • Si la fonction devait de toute façon faire une copie de la référence const, prenez-la par valeur.

"Primitif" ci-dessus signifie essentiellement de petits types de données qui font quelques octets de long et qui ne sont pas 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ù l'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 nativement avec le support du langage. Jusque-là, j'irais avec les règles ci-dessus.

Exemples: Pour rendre une chaîne en majuscules et renvoyer la version en majuscules, 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 pour l'appelant et faire cette copie tôt afin que l'appelant puisse optimiser autant que possible - comme détaillé dans cet article:

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

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

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

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

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
Johannes Schaub - litb
la source
J'ai trouvé vos règles bonnes mais je ne suis pas sûr de la première partie où vous parlez de ne pas le passer comme référence pour l'accélérer. oui bien sûr, mais ne pas passer quelque chose comme une référence juste cus d'optimaztion n'a pas de sens du tout. si vous voulez changer l'objet de pile que vous passez, faites-le par ref. si vous ne le faites pas, passez-le par valeur. si vous ne voulez pas le changer, passez-le comme const-ref. l'optimisation qui vient avec la valeur de passage ne devrait pas avoir d'importance puisque vous gagnez d'autres choses en passant comme ref. je ne comprends pas le "veux de la vitesse?" Sice si vous alliez effectuer ces opérations, vous
passeriez
Johannes: J'ai adoré cet article quand je l'ai lu, mais j'ai été déçu quand je l'ai essayé. Ce code a échoué sur GCC et MSVC. Ai-je raté quelque chose ou cela ne fonctionne-t-il pas en pratique?
user541686
Je ne pense pas que je convienne que si vous voulez faire une copie de toute façon, vous devez la passer par valeur (au lieu de const ref), puis la déplacer. Regardez-le de cette façon, qu'est-ce qui est plus efficace, une copie et un mouvement (vous pouvez même avoir 2 copies si vous le passez en avant), ou juste une copie? Oui, il y a des cas spéciaux de chaque côté, mais si vos données ne peuvent pas être déplacées de toute façon (ex: un POD avec des tonnes d'entiers), pas besoin de copies supplémentaires.
Ion Todirel
2
Mehrdad, je ne sais pas ce que vous
attendiez
Je considérerais la nécessité de copier juste pour convaincre le compilateur que les types ne chevauchent pas une lacune dans le langage. Je préfère utiliser GCC __restrict__(qui peut également fonctionner sur des références) plutôt que de faire trop de copies. Dommage que le standard C ++ n'ait pas adopté le restrictmot clé C99 .
Ruslan
12

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

Lou Franco
la source
Pour les types non natifs, vous pouvez (en fonction de la façon dont le compilateur optimise le code) obtenir une augmentation des performances en utilisant des références const au lieu de simplement des références.
JO.
9

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 certaines très petites structures, comme une paire d'ts, peuvent mieux fonctionner en passant par la valeur.

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

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

Torlack
la source
Remarque sur la terminologie: une structure contenant un million d'ints est toujours un "type POD". Peut-être que vous voulez dire «pour les types intégrés, il est préférable de passer par la valeur».
Steve Jessop
6

Voici ce que je travaille normalement lors de la conception de l'interface d'une fonction non-modèle:

  1. Passez par valeur si la fonction ne veut pas modifier le paramètre et que la valeur est bon marché à copier (int, double, float, char, bool, etc ... Notez que std :: string, std :: vector et le reste des conteneurs de la bibliothèque standard ne sont PAS)

  2. Passez le pointeur const si la valeur est coûteuse à 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 un pointeur non const si la valeur est coûteuse à copier et que la fonction souhaite modifier la valeur pointée et que NULL est une valeur que la fonction gère.

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

  5. Passez par une référence non constante lorsque la valeur est coûteuse à copier et que la fonction souhaite modifier la valeur à laquelle il est fait référence et que NULL ne serait pas une valeur valide si un pointeur était utilisé à la place.

Martin G
la source
Ajoutez std::optionalà l'image et vous n'avez plus besoin de pointeurs.
Violet Giraffe
5

On dirait que vous avez obtenu votre réponse. Passer par la valeur coûte cher, mais vous en donne une copie si vous en avez besoin.

GeekyMonkey
la source
Je ne sais pas pourquoi cela a été rejeté? Ça me semble logique. Si vous allez avoir besoin de la valeur actuellement stockée, passez par valeur. Sinon, passez la référence.
Totty
4
Il dépend totalement du type. Faire un type POD (plain old data) par référence peut en fait réduire les performances en provoquant plus d'accès à la mémoire.
Torlack
1
Évidemment, passer int par référence ne sauve rien! Je pense que la question implique des choses qui sont plus grandes qu'un pointeur.
GeekyMonkey
4
Ce n'est pas si évident, j'ai vu beaucoup de code par des gens qui ne comprennent pas vraiment comment fonctionnent les ordinateurs en passant des choses simples par const ref parce qu'on leur a dit que c'était la meilleure chose à faire.
Torlack
4

En règle générale, passer par référence const est préférable. Mais si vous devez 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. Référencer réellement représenté en interne par un pointeur, c'est pourquoi vous pouvez vous attendre par exemple à ce que pour le pointeur les deux passages soient les mêmes en termes de performances, ou même le passage par valeur peut être plus rapide en raison de la déférence inutile.

sergtk
la source
Si vous devez modifier la copie du paramètre de l'appelé, vous pouvez effectuer une copie dans le code appelé plutôt que de passer par valeur. IMO, vous ne devriez généralement pas choisir l'API en fonction d'un détail d'implémentation comme celui-ci: la source du code appelant est la même dans les deux cas, mais pas son code objet.
Steve Jessop
Si vous passez par la valeur, une copie est créée. Et IMO, il n'y a pas d'importance dans la façon dont vous créez une copie: via un argument passant par valeur ou localement - c'est ce qui concerne le C ++. Mais du point de vue du design, je suis d'accord avec vous. Mais je ne décris ici que les fonctionnalités C ++ et ne touche pas au design.
sergtk
1

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

Peter
la source
Ma compréhension est que std::vectorréellement alloue ses éléments sur le tas et l'objet vectoriel lui-même ne se développe jamais. Oh, attendez. Si l'opération entraîne une copie du vecteur, elle ira en fait dupliquer tous les éléments. Ce serait mauvais.
Steven Lu
1
Oui, c'est ce que je pensais. sizeof(std::vector<int>)est constant, mais le transmettre par valeur copiera toujours le contenu en l'absence de toute intelligence du compilateur.
Peter
1

Passez par valeur pour les petits types.

Passez les références const pour les gros types (la définition de big peut varier selon les machines) MAIS, en C ++ 11, passez par valeur si vous allez consommer les données, car vous pouvez exploiter la sémantique des mouvements. 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_de la classe Person. Si vous passez par référence const, une copie devra être faite pour le mettre dedans name_.

Germán Diago
la source
-5

Différence simple: - En fonction, nous avons un paramètre d'entrée et de sortie, donc si votre paramètre d'entrée et de sortie est le même, utilisez appel par référence sinon si les paramètres d'entrée et de sortie sont différents, il vaut mieux utiliser l'appel par valeur.

exemple void amount(int account , int deposit , int total )

paramètre d'entrée: compte, paramètre de sortie de dépôt: total

entrée et sortie est un appel d'utilisation différent par vaule

  1. void amount(int total , int deposit )

total d'entrée dépôt total de sortie

Dhirendra Sengar
la source