J'ai remarqué que j'utilise habituellement des références constantes comme valeurs de retour ou arguments. Je pense que la raison en est que cela fonctionne presque de la même manière que l'utilisation de la non-référence dans le code. Mais cela prend définitivement plus d'espace et les déclarations de fonctions deviennent plus longues. Je suis OK avec un tel code mais je pense que certaines personnes le trouvent un mauvais style de programmation.
Qu'est-ce que tu penses? Vaut-il la peine d'écrire const int & over int? Je pense qu'il est de toute façon optimisé par le compilateur, alors peut-être que je perds juste mon temps à le coder, a?
En C++, il est très courant ce que je considère comme un anti-modèle qui utilise const T&
comme une façon intelligente de dire simplement T
quand on traite des paramètres. Cependant, une valeur et une référence (peu importe si const ou non) sont deux choses complètement différentes et utiliser toujours et aveuglément des références au lieu de valeurs peut conduire à des bogues subtils.
La raison en est que lorsque vous traitez des références, vous devez considérer deux problèmes qui ne sont pas présents avec des valeurs: durée de vie et aliasing.
Tout comme un exemple, un endroit où cet anti-modèle est appliqué est la bibliothèque standard elle-même, où std::vector<T>::Push_back
accepte comme paramètre un const T&
au lieu d'une valeur et cela peut mordre par exemple dans du code comme:
std::vector<T> v;
...
if (v.size())
v.Push_back(v[0]); // Add first element also as last element
Ce code est une bombe à retardement car std::vector::Push_back
veut une référence const mais faire le Push_back peut nécessiter une réallocation et si cela se produit signifie qu'après la réallocation la référence reçue ne serait plus valide (( durée de vie problème) et vous entrez dans le domaine du comportement indéfini.
Les problèmes d'alias sont également une source de problèmes subtils si des références const sont utilisées à la place des valeurs. J'ai été mordu par exemple par un code de ce genre:
struct P2d
{
double x, y;
P2d(double x, double y) : x(x), y(y) {}
P2d& operator+=(const P2d& p) { x+=p.x; y+=p.y; return *this; }
P2d& operator-=(const P2d& p) { x-=p.x; y-=p.y; return *this; }
};
struct Rect
{
P2d tl, br;
Rect(const P2d& tl, const P2d& br) : tl(tl), bt(br) {}
Rect& operator+=(const P2d& p) { tl+=p; br+=p; return *this; }
Rect& operator-=(const P2d& p) { tl-=p; br-=p; return *this; }
};
Le code semble à première vue assez sûr, P2d
est un point bidimensionnel, Rect
est un rectangle et ajouter/soustraire un point signifie traduire le rectangle.
Si toutefois pour traduire le rectangle dans l'origine, vous écrivez myrect -= myrect.tl;
le code ne fonctionnera pas car l'opérateur de traduction a été défini en acceptant une référence qui (dans ce cas) fait référence à un membre de la même instance.
Cela signifie qu'après la mise à jour du topleft avec tl -= p;
le topleft sera (0, 0)
comme il se doit mais aussi p
deviendra en même temps (0, 0)
car p
n'est qu'une référence au membre en haut à gauche et donc la mise à jour du coin en bas à droite ne fonctionnera pas car elle le traduira par (0, 0)
ne faisant donc pratiquement rien.
Ne vous laissez pas berner en pensant qu'une référence const est comme une valeur à cause du mot const
. Ce mot existe uniquement pour vous donner des erreurs de compilation si vous essayez de modifier l'objet référencé en utilisant cette référence , mais ne signifie pas que l'objet référencé est constant . Plus précisément, l'objet référencé par une référence const peut changer (par exemple en raison de l'aliasing ) et peut même disparaître pendant que vous l'utilisez ( durée de vie problème).
Dans const T&
le mot const exprime une propriété de la référence , pas de la référence référencée objet : c'est la propriété qui rend impossible de l'utiliser pour changer l'objet. Probablement en lecture seule aurait été un meilleur nom car const a l'OMI l'effet psychologique de pousser l'idée que l'objet va être constant pendant que vous utilisez la référence.
Vous pouvez bien sûr obtenir des accélérations impressionnantes en utilisant des références au lieu de copier les valeurs, en particulier pour les grandes classes. Mais vous devez toujours penser aux problèmes d'alias et de durée de vie lorsque vous utilisez des références, car sous le capot, ce ne sont que des pointeurs vers d'autres données. Pour les types de données "natifs" (entiers, doubles, pointeurs), les références seront en fait plus lentes que les valeurs et il n'y a rien à gagner à les utiliser à la place des valeurs.
De plus, une référence const signifie toujours des problèmes pour l'optimiseur car le compilateur est forcé d'être paranoïaque et chaque fois qu'un code inconnu est exécuté, il doit supposer que tous les objets référencés peuvent maintenant avoir une valeur différente (const
pour une référence signifie absolument RIEN pour l'optimiseur; que Word n'est là que pour aider les programmeurs - personnellement, je ne suis pas sûr que ce soit une grande aide, mais c'est une autre histoire).
Comme le dit Oli, renvoyer un const T&
par opposition à T
sont des choses complètement différentes, et peuvent casser dans certaines situations (comme dans son exemple).
Prendre const T&
par opposition à plain T
en tant qu'argument est moins susceptible de casser les choses, mais présente encore plusieurs différences importantes.
T
au lieu de const T&
requiert que T
soit constructible par copie.T
invoquera le constructeur de copie, qui peut être coûteux (ainsi que le destructeur à la sortie de la fonction).T
vous permet de modifier le paramètre en tant que variable locale (peut être plus rapide que la copie manuelle).const T&
pourrait être plus lent en raison de décalages temporaires et du coût de l'indirection.Si l'appelé et l'appelant sont définis dans des unités de compilation distinctes, le compilateur ne peut pas optimiser la référence. Par exemple, j'ai compilé le code suivant:
#include <ctime>
#include <iostream>
int test1(int i);
int test2(const int& i);
int main() {
int i = std::time(0);
int j = test1(i);
int k = test2(i);
std::cout << j + k << std::endl;
}
avec G ++ sur Linux 64 bits au niveau d'optimisation 3. Le premier appel n'a pas besoin d'accéder à la mémoire principale:
call time
movl %eax, %edi #1
movl %eax, 12(%rsp) #2
call _Z5test1i
leaq 12(%rsp), %rdi #3
movl %eax, %ebx
call _Z5test2RKi
La ligne # 1 utilise directement la valeur de retour dans eax
comme argument pour test1
dans edi
. Ligne # 2 et # 3 Poussez le résultat dans la mémoire principale et placez l'adresse dans le premier argument car l'argument est déclaré comme référence à int, et il doit donc être possible de p. Ex. prendre son adresse. Que quelque chose puisse être calculé entièrement à l'aide de registres ou qu'il soit nécessaire d'accéder à la mémoire principale peut faire une grande différence de nos jours. Donc, en plus d'être plus à taper, const int&
peut également être plus lent. La règle générale est de transmettre toutes les données qui sont au plus aussi grandes que la taille de Word par valeur, et tout le reste par référence à const. Passez également des arguments modèles par référence à const; puisque le compilateur a accès à la définition du modèle, il peut toujours optimiser la référence.
int &
et int
ne sont pas interchangeables! En particulier, si vous renvoyez une référence à une variable de pile locale, le comportement n'est pas défini, par exemple:
int &func()
{
int x = 42;
return x;
}
Vous pouvez renvoyer une référence à quelque chose qui ne sera pas détruit à la fin de la fonction (par exemple, un élément statique ou un membre de classe). C'est donc valable:
int &func()
{
static int x = 42;
return x;
}
et vers le monde extérieur, a le même effet que de renvoyer directement le int
(sauf que vous pouvez maintenant le modifier, c'est pourquoi vous voyez const int &
beaucoup).
L'avantage de la référence est qu'aucune copie n'est requise, ce qui est important si vous traitez avec des objets de grande classe. Cependant, dans de nombreux cas, le compilateur peut optimiser cela; voir par exemple http://en.wikipedia.org/wiki/Return_value_optimization .
Au lieu de "penser", il est optimisé par le compilateur, pourquoi ne pas obtenir la liste des assembleurs et le vérifier avec certitude?
junk.c ++:
int my_int()
{
static int v = 5;
return v;
}
const int& my_int_ref()
{
static int v = 5;
return v;
}
Sortie assembleur générée (élidée):
_Z6my_intv:
.LFB0:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
movl $5, %eax
ret
.cfi_endproc
...
_Z10my_int_refv:
.LFB1:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
movl $_ZZ10my_int_refvE1v, %eax
ret
Les instructions movl
des deux sont très différentes. Les premiers mouvements 5
dans EAX
(qui se trouve être le registre traditionnellement utilisé pour renvoyer des valeurs dans le code C x86) et le second déplace l'adresse d'une variable (spécificités éligibles pour plus de clarté) dans EAX
. Cela signifie que la fonction appelante dans le premier cas peut simplement utiliser directement les opérations de registre sans toucher à la mémoire pour utiliser la réponse tandis que dans le second, elle doit frapper la mémoire via le pointeur renvoyé.
Il semble donc qu'il ne soit pas optimisé.
C'est en plus des autres réponses qui vous ont été données ici expliquant pourquoi T
et const T&
ne sont pas interchangeables.
int est différent avec const int &:
2, int est la copie de valeur d'une autre variable entière (int B), ce qui signifie: si nous changeons int B, la valeur de int ne changera pas.
Voir le code c ++ suivant:
int main(){
vecteur a {1,2,3};
int b = a [2]; // la valeur ne change pas même lorsque le vecteur change
const int & c = a [2]; // ceci est une référence, donc la valeur dépend du vecteur;
a [2] = 111;
// b affichera 3;
// c affichera 111;
}