Références C ++ – sont-elles juste du sucre syntaxique?

Une référence en C ++ n’est-elle qu’un sucre syntaxique ou offre-t-elle des accélérations dans certains cas?

Par exemple, un appel par pointeur implique de toute façon une copie, et cela semble également être le cas pour un appel par référence. Le mécanisme sous-jacent semble être le même.

Edit: Après environ six réponses et de nombreux commentaires. Je suis encore d’avis que les références ne sont que du sucre syntatique. Si les gens pouvaient répondre par un oui ou un non, et si quelqu’un pouvait faire une réponse acceptée?

Les références ont des garanties plus fortes que les pointeurs, de sorte que le compilateur peut optimiser de manière plus agressive. J’ai récemment vu GCC incorporer plusieurs appels nesteds par le biais de références de fonctions, mais pas un seul via des pointeurs de fonctions (car il ne pouvait pas prouver que le pointeur pointait toujours vers la même fonction).

Si la référence est stockée quelque part, elle prend généralement le même espace qu’un pointeur. Cela ne veut pas dire, encore une fois, qu’il sera utilisé comme un pointeur: le compilateur pourrait bien le couper s’il sait à quel object la référence était liée.

Supposez une référence en tant que pointeur qui:

  1. Ne peut pas être nul
  2. Une fois initialisé, ne peut pas être redirigé vers un autre object
  3. Toute tentative d’utilisation de celui-ci le déroutera implicitement:

     int a = 5; int &ra = a; int *pa = &a; ra = 6; (*pa) = 6; 

ici comme il regarde dans le déassembly:

  int a = 5; 00ED534E mov dword ptr [a],5 int &ra = a; 00ED5355 lea eax,[a] 00ED5358 mov dword ptr [ra],eax int *pa = &a; 00ED535B lea eax,[a] 00ED535E mov dword ptr [pa],eax ra = 6; 00ED5361 mov eax,dword ptr [ra] 00ED5364 mov dword ptr [eax],6 (*pa) = 6; 00ED536A mov eax,dword ptr [pa] 00ED536D mov dword ptr [eax],6 

l’affectation à la référence est la même chose depuis la perspective du compilateur que l’affectation à un pointeur déréférencé. Il n’y a pas de différence entre eux, comme vous pouvez le voir (nous ne parlons pas de l’optimisation du compilateur pour le moment).

En ce qui me concerne, je préfère utiliser des références tant que je n’ai pas besoin de nullptr comme valeur valide, de valeurs qui doivent être repointées ou de valeurs de types différents à transmettre (par exemple, un pointeur vers un type d’interface).

Le compilateur ne peut pas supposer qu’un pointeur est non nul. lors de l’optimisation du code, il doit soit prouver que le pointeur est non nul, soit émettre un programme qui tient compte de la possibilité qu’il soit nul (dans un contexte où cela serait bien défini).

De même, le compilateur ne peut pas supposer que le pointeur ne change jamais de valeur. (il ne peut pas non plus supposer que le pointeur pointe vers un object valide, bien que j’aie du mal à imaginer un cas où cela aurait de l’importance dans un contexte bien défini)

D’un autre côté, en supposant que les références sont implémentées en tant que pointeurs, le compilateur est toujours autorisé à supposer qu’il est non nul, ne change jamais où il pointe et pointe vers un object valide.

Les références diffèrent des pointeurs en ce sens qu’il y a des choses que vous ne pouvez pas faire avec une référence et qui ont un comportement défini.

Vous ne pouvez pas prendre l’adresse d’une référence, mais uniquement ce qui est mentionné. Vous ne pouvez pas modifier une référence une fois qu’elle est créée.

Un T& et un T*const (notez que const s’applique au pointeur, pas le pointé, y sont) sont relativement similaires. Prendre l’adresse d’une valeur de const réelle et la modifier est un comportement indéfini, tout comme la modification (tout stockage utilisé directement) une référence.

Maintenant, en pratique, vous pouvez obtenir une référence:

 struct foo { int& x; }; 

sizeof(foo) sera presque certainement égal à sizeof(int*) . Mais le compilateur est libre de négliger la possibilité que quelqu’un accédant directement aux octets de foo puisse réellement changer la valeur à laquelle il est fait référence. Cela permet au compilateur de lire l’implémentation de “pointeur” de référence une fois, puis de ne jamais le relire. Si nous avions struct foo{ int* x; } struct foo{ int* x; } le compilateur devrait prouver chaque fois qu’il a fait un *fx que la valeur du pointeur n’a pas changé.

Si vous aviez struct foo{ int*const x; } struct foo{ int*const x; } recommence à se comporter comme une référence dans son immutabilité (modifier quelque chose qui a été déclaré const est UB).


Une astuce que je ne connais pas des auteurs de compilateurs utilisant est de compresser la capture de référence dans un lambda.

Si vous avez un lambda qui capture des données par référence, au lieu de capturer chaque valeur via un pointeur, il ne peut capturer que le pointeur de trame de la stack. Les décalages pour chaque variable locale sont des constantes à la compilation du pointeur de trame de stack.

L’exception concerne les références capturées par référence, qui sous un rapport de défauts à C ++ doivent restr valides même si la variable de référence est hors de scope. Donc, ceux-ci doivent être capturés par un pseudo-pointeur.

Pour un exemple concret (si celui-ci est un jouet):

 void part( std::vector& v, int left, int right ) { std::function op = [&](int y){return yright;}; std::partition( begin(v), end(v), op ); } 

le lambda ci-dessus ne peut capturer que le pointeur de la trame de la stack, et savoir où il se trouve par rapport à left et à right , en réduisant sa taille, au lieu de capturer deux références int .

Nous avons ici des références implicites par [&] dont l’existence est éliminée plus facilement que si elles étaient des pointeurs capturés par valeur:

 void part( std::vector& v, int left, int right ) { int* pleft=&left; int* pright=&right; std::function op = [=](int y){return y<*pleft && y>*pright;}; std::partition( begin(v), end(v), op ); } 

Il existe quelques autres différences entre les références et les pointeurs.

Une référence peut prolonger la durée de vie d’un temporaire.

Ceci est fortement utilisé for(:) boucles for(:) . La définition de la boucle for(:) repose sur l’extension de la durée de vie de référence pour éviter les copies inutiles, et les utilisateurs de boucles for(:) peuvent utiliser auto&& pour déduire automatiquement le moyen le plus léger d’encapsuler les objects itérés.

 struct big { int data[1<<10]; }; std::array arr; arr get_arr(); for (auto&& b : get_arr()) { } 

ici, la prolongation de la durée de vie de référence empêche les copies inutiles de se produire. Si nous modifions make_arr pour renvoyer un arr const& il continue de fonctionner sans aucune copie. Si nous modifions get_arr pour retourner un conteneur qui renvoie des éléments big à la valeur (disons, une plage d’iterators d’entrée), encore une fois, aucune copie inutile n’est effectuée.

C’est en quelque sorte le sucre syntaxique, mais cela permet d’optimiser le même concept dans de nombreux cas sans avoir à micro-optimiser en fonction de la manière dont les choses sont renvoyées ou répétées.


De même, les références de transfert permettent de traiter intelligemment les données en tant que const, non-const, lvalue ou rvalue. Les temporaires sont marqués comme temporaires, les données dont les utilisateurs n’ont plus besoin sont marquées comme temporaires, les données qui persistent sont marquées comme étant une référence lvalue.

L’avantage que les références ont sur les non-références ici est que vous pouvez former une référence à une valeur temporaire, et vous ne pouvez pas créer un pointeur sur cette référence temporaire sans passer par une conversion de référence de référence à lvalue.

Non


Les références ne sont pas simplement une différence syntaxique ; ils ont aussi une sémantique différente:

  • Une référence alias toujours un object existant, contrairement à un pointeur qui peut être nullptr (une valeur sentinelle).
  • Une référence ne peut pas être réinstallée, elle pointe toujours vers le même object pendant toute sa durée de vie.
  • Une référence peut prolonger la durée de vie d’un object, voir la liaison à auto const& ou auto&& .

Ainsi, au niveau du langage, une référence est une entité propre. Le rest sont des détails de mise en œuvre.

Avant, le compilateur optimisait les références. Cependant, les compilateurs modernes sont devenus si bons qu’il n’y a plus aucun avantage.

Un avantage énorme que les références ont sur les pointeurs est qu’une référence peut faire référence à une valeur dans un registre, tandis que les pointeurs ne peuvent pointer que vers des blocs de mémoire. Prenez l’adresse de quelque chose qui aurait été dans un registre, et vous forceriez le compilateur à placer cette valeur dans un emplacement de mémoire normal. Cela peut créer des avantages considérables dans les boucles serrées.

Cependant, les compilateurs modernes sont si bons qu’ils reconnaissent maintenant un pointeur qui pourrait être une référence à toutes fins utiles, et le traitent exactement comme s’il s’agissait d’une référence. Cela peut entraîner des résultats assez insortinggants dans un débogueur, où vous pouvez avoir une instruction telle que int* p = &x , demander au débogueur d’imprimer la valeur de p , seulement pour qu’il dise quelque chose comme “p ne peut pas être imprimé” car x était en fait dans un registre et le compilateur traitait *p comme une référence à x ! Dans ce cas, il n’y a littéralement aucune valeur pour p

(Cependant, si vous essayiez de faire de l’arithmétique de pointeur sur p , vous forceriez alors le compilateur à ne plus optimiser le pointeur pour agir comme une référence, et tout ralentirait)

8.3.2 Références [dcl.ref]

Une référence peut être considérée comme un nom d’object

qui est différent des pointeurs, qui est une variable (contrairement à la référence) qui contient l’adresse d’un emplacement mémoire d’un object ** . Le type de cette variable est le pointeur sur Object.

La référence interne peut être implémentée comme pointeur, mais la norme ne le garantit jamais.

Donc, pour répondre à votre question: la référence C ++ n’est pas du sucre syntaxique aux pointeurs. Et si elle offre une accélération a déjà été répondu en profondeur.

****** L’object ici signifie toute instance ayant une adresse mémoire. Même les pointeurs sont des objects, de même que les fonctions (et donc nous avons des pointeurs et des pointeurs de fonctions nesteds). Dans le même sens, nous n’avons pas de pointeurs à référencer car ils ne sont pas instanciés.