Quelqu'un pourrait m'expliquer pourquoi en C++ il y a la possibilité de passer des objets par valeur et par référence alors que d'après ce que j'ai lu, le passage par référence est toujours plus efficace ?
Si l'objet est de petite taille (comme un int ou un double) est-il vraiment plus efficace de le passer par valeur ou est-ce équivalent de le passer par référence ?
Il existe des cas où le passage par référence est nécessaire (A), et il existe des cas où on a le choix (B) et peut-être d'autres cas (C).
On peut distinguer les cas de passage de paramètres de fonction en: V) passage par valeur, RM) passage par référence mutable RC) passage par référence constante RR) passage par right-référence RF) passage par forward-reference
1) Si la fonction veut modifier l'objet original (cas A) - on doit utiliser le (RM)
2) Si la fonction veut seulement lire l'objet original (cas B) - on utilise (RC) ou bien (V), pour diverses raisons on peut préférer l'un à l'autre.
int carre( int x ) { // préféré
return x * x;
}
int carre( int const& x ) {
return x * x;
}
void afficher( std::vector<int> v ) {
std::copy( begin(v), end(v), std::ostream_iterator<int>( std::cout, ",") );
}
void afficher( std::vector<int>const& const v ) { // préféré
std::copy( begin(v), end(v), std::ostream_iterator<int>( std::cout, ",") );
}
Parfois, le choix est limite. Il y a un article au sujet de std::string ou std::string const& de Herb Sutter à ce sujet. Ou un article de Dave Abrahams qui montre que le passage par copie est souvent à préférer.
3) La fonction veut mémoriser une copie l'objet On peut aussi utiliser (RC) ou (V), mais (V) sera toujours plus optimum et est à préférer.
4) Si la fonction veut retenir l'objet pour y revenir plus tard - ça sera (RC) si l'objet est seulement lu, ou (RM) s'il sera modifié.
5) Si la fonction veut s'approprier l'objet (donc l'appelant ne va plus l'utiliser.) - ça sera (RR) - mais on peut aussi se ramener à (4).
6) Le cas (RF) n'est utilisé qu'en template, il permet de transmettre le besoin (2)(3)(4)ou(5) à une fonction interne.
Pour certains types d'objets, on n'a pas toutes les possibilités: Par exemple: std::unique_ptr<>, std::initializer_list<> ou les vues, sont presque toujours à passer par valeur (V) Par exemple: Les objets non copiables ne peuvent pas être passés par valeur (QWidget, std::ostream<>, ...)
Si on regarde les codes, on voit que le passage par valeur reste le plus utilisé (car on passe très souvent des objets simples en lecture)
Pour faire simple: au niveau du code binaire exécutable (comprend: le code qui sera effectivement utilisé par le programme, une fois compilé, pour s'exécuter), une référence n'est en réalité qu'une adresse mémoire. Exactement comme les pointeurs, en fait ;).
Seulement, une référence va offrir offrir certains aspects que le pointeur n'offre pas comme:
le fait que la référence est d'office l'adresse d'un objet qui existe
le fait que l'on peut la manipuler (au niveau du code que nous écrivons) exactement comme s'il s'agissait de l'objet lui-même (c'est pour cela que l'on dit qu'une référence est un alias de l'objet référencé)
Et donc, ce qu'il faut bien comprendre, c'est que, lorsque l'on décide de passer une donnée par référence, le code va forcément utiliser "un peu plus de mémoire" -- ce qui correspond à la taille d'une adresse pour le programme -- et une indirection de plus que si la donnée avait été transmise par valeur.
Cela ne va poser aucun problème, pour toute les données "plus grosse qu'un type primitif", celles qui vont nécessiter une taille supérieure à la taille de cette adresse mémoire pour pouvoir être représentées en mémoire.
Par contre, il est souvent "dommage" de transmettre l'adresse (qui prend "une certaine place en mémoire) d'une donnée qui ne prend -- a priori -- pas plus (ou tout juste autant) d'espace mémoire que la taille de cette adresse pour pouvoir être représentée en mémoire. On a "tout aussi vite fait" de transmettre cette donnée ... par valeur plutôt que par référence, surtout si la fonction n'a pas pour but de modifier la valeur d'origine.
Au final, on peut dire qu'une règle "cohérente" serait de transmettre "tout ce qui est plus gros qu'une adresse mémoire" par référence (éventuellement constante) et "tout ce qui n'est pas plus gros qu'une adresse mémoire" par valeur.
Et comme la taille réelle d'une adresse mémoire risque de varier d'un système à l'autre, on peut partir du principe de transmettre tous les types primitifs (char, short, int, long, long long, float, double et long double) ainsi que les énumérations (qui ne sont que des valeurs numériques entières, au final) par valeur et tous les types "définis par l'utilisateur" (toutes les structures et les classes) par référence
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Le fait est qu'il y a énormément de raisons qui peuvent faire que l'on ne souhaite, bien souvent, pas copier une structure ou une classe. Parmi celles-ci, on peut citer:
le fait que la donnée ne peut -- tout simplement -- pas être copiée, pour des besoins d'unicité, à cause de sa sémantique propre
les problèmes liés à la "désynchronisation" potentielle de la donnée d'origine et de sa copie
le fait que la copie est -- peut-être -- un processus "couteux", que ce soit en temps ou en ressources
j'en passe, et sans doute de meilleures
Mais, si toutes ces raisons font sens pour les types définis par l'utilisateur (qui ne sont pas de "simples" valeurs numériques, comme les énumérations), elles ne font en revanche que très peu pour les données "simples", comme les types primitifs (ou les énumérations, justement).
Alors, je suis d'accord que mon explication porte à croire qu'il s'agit d'une micro-optimisation, ce que c'est d'ailleurs peut-être au demeurant, mais je crois qu'il était nécessaire d'introduire cette idée de "taille d'une adresse en mémoire" pour faire comprendre les raisons de la règle telle que je l'ai exprimée
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Facteur taille : il s'y ajoute celui du coût supplémentaire de manipulation de la valeur par indirection (accès memoire), et, pour faire bon poids, la taille du code généré.
Dans la plupart des contextes, ça va changer les performances des programmes de quelques millièmes.
Donc si on veut optimiser le rendement de temps passé à programmer, c'est rarement une bonne idée de trop se prendre le chou avec, et on fait avec la règle "Si c'est un type primitif, par copie. Si c'est un objet, par référence constante".
En cas de doute : on écrit les deux versions, et on mesure les performances. Si vous avez la flemme de tester, c'est que ça n'en valait pas la peine.
- Edité par michelbillaud 13 septembre 2021 à 8:59:04
Pourquoi passer des objets par valeur en C++ ?
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
En recherche d'emploi.