Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Question newbie : référence comme retour de fonction

4 réponses
Avatar
meow
Bonjour,

Juste pour etre certain d'avoir bien compris la chose, dans le cas
de l'exemple ci-dessous tout se passe comme si sommet() passait un
pointeur sur la pile, et qu'on d=E9r=E9f=E9ran=E7ait =E0 l'arriv=E9e pour
initialiser p !?

En clair : qu'est-ce qui passe sur la pile : un Point ou une adresse ?

-----------------------
class A{
Point _p;

[=2E.]

Point& sommet() {return(_p);};
}


[=2E.]
A a;
[=2E.]
Point p;
p =3D a.sommet();
------------------------

4 réponses

Avatar
kanze
meow wrote:

Juste pour etre certain d'avoir bien compris la chose, dans
le cas de l'exemple ci-dessous tout se passe comme si sommet()
passait un pointeur sur la pile, et qu'on déréférançait à
l'arrivée pour initialiser p !?

En clair : qu'est-ce qui passe sur la pile : un Point ou une adresse ?


Je ne comprends pas trop la question. Quand on parle de la pile,
on parle habituellement des variables locales et des
temporaires. Typiquement aussi, une référence s'implémente comme
un pointeur, dans les cas où le compilateur ne réussit pas à
l'éliminer complétement.

-----------------------
class A{
Point _p;

[..]

Point& sommet() {return(_p);};
}


[..]
A a;


En supposant que tu es dans une fonction, ici `a' se trouvera
sur la pile. En dehors d'une fonction, il se trouvera dans la
mémoire statique.

[..]
Point p;


Même chose pour p.

p = a.sommet();


Dans une implémentation typique, le compilateur passera
l'adresse de a comme premier paramètre à sommet, pour devenir le
pointeur this. Comment dépend des conventions de l'API en
question. Sur mon Sparc, c'est dans le régistre o0 (qui devient
i0 dans la fonction appelée). Sur des anciens compilateurs
Intel, on le pushait en dernier sur la pile, mais j'imagine que
les choses se sont améliorées depuis -- EBX me semble
naturellement indiqué.

Grosso modo, dans la fonction, le compilateur génèrera du code
pour calculer l'adresse de _p à partir du pointeur this -- le
paramètre ci-dessus. Et il mettra le résultat de ce calcul où
l'API veut qu'on met les valeurs de rétour de type pointeur : le
régistre i0 sur un Sparc, EAX (je crois) sur un IA-32, etc.
(Ici, je n'ai jamais vu de cas où ce n'était pas dans un
régistre.)

Enfin, le compilateur génèrera du code pour appeler l'opérateur
d'affectation de Point, avec l'adresse de p comme premier
paramètre, et la valeur renvoyée de l'appel de sommet comme
deuxième paramètre. Ici aussi, où il met les paramètres dépend
de l'API en question.

Et évidemment, si la fonction est inline, comme ici, le
compilateur risque de faire pas mal de raccourcis. Si Point est
quelque chose de simple, disons deux double, et son opérateur
d'affectation est celui par défaut, par exemple, le compilateur
va sans doute générer le même code qu'il ferait pour deux
affectations de double, sans que la pile entre en jeu pour rien.

------------------------


--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
meow
Merci pour cette réponse rapide :)

Je ne comprends pas trop la question.
J'ai conscience de n'etre pas très clair, c'est en partie du a mon

inexpérience dans le domaine. je vais essayer de faire mieux.

Quand on parle de la pile,
on parle habituellement des variables locales et des
temporaires.
Ma connaissance en ce domaine est très limitée, mais il me semble me

souvenir que la pile est cette partie de la mémoire sur laquelle on
empile et on dépile les variables au grès de leur portée... En
particulier, on entasse en strate le contexte des fonctions appelantes,
et lors du retour d'une fonction, en gros (et certainement aussi un peu
en faux), on dépile tout le contexte de ladite fonction et on empile
son résultat. Méthode par laquelle la fonction appelante reçoit le
résultat.
Et donc, si j'ai bien tout compris à ce qu'on m'a enseigné, une des
raisons pour utiliser des pointeurs plutot que des objets comme valeur
de retour, c'est qu'au lieu de copier tout l'objet sur la pile, on en
copie que l'adresse...

La question que je me posais était de savoir ce qu'il se passait dans
le cas d'une référence au lieu d'un pointeur comme valeur de retour
d'une fonction.

Typiquement aussi, une référence s'implémente comme
un pointeur, dans les cas où le compilateur ne réussit pas à
l'éliminer complétement.
Dans ce cas, je supposes que tout se passe plus ou moins comme si

j'avais écrit:
Point* sommet() {return(&_p)}
puis dans mon programme :
point *pp=a.sommet();
bref,


[..]
A a;
En supposant que tu es dans une fonction, ici `a' se trouvera

sur la pile. En dehors d'une fonction, il se trouvera dans la
mémoire statique.
Oui, autant pour moi, j'aurais du préciser que je plaçais la partie

du code non incluse dans la classe dans un bloc main(){}...

p = a.sommet();
[..]


Enfin, le compilateur génèrera du code pour appeler l'opérateur
d'affectation de Point, avec l'adresse de p comme premier
paramètre, et la valeur renvoyée de l'appel de sommet comme
deuxième paramètre. Ici aussi, où il met les paramètres dépend
de l'API en question.
[..]

Si je vous comprend bien, le compilateur fait une différence entre les
valeurs de retour de type pointeur -remisées dans un registre- et les
autres : posées sur la pile ?

--Ben


Avatar
kanze
meow wrote:
Je ne comprends pas trop la question.


J'ai conscience de n'etre pas très clair, c'est en partie du a
mon inexpérience dans le domaine. je vais essayer de faire
mieux.


Je m'en doutais un peu. Sans un minimum de connaissances, c'est
difficile même à formuler les bonnes questions.

Quand on parle de la pile, on parle habituellement des
variables locales et des temporaires.


Ma connaissance en ce domaine est très limitée, mais il me
semble me souvenir que la pile est cette partie de la mémoire
sur laquelle on empile et on dépile les variables au grès de
leur portée... En particulier, on entasse en strate le
contexte des fonctions appelantes, et lors du retour d'une
fonction, en gros (et certainement aussi un peu en faux), on
dépile tout le contexte de ladite fonction et on empile son
résultat. Méthode par laquelle la fonction appelante reçoit le
résultat.


Par pile, on entend une de deux choses (apparentées,
d'ailleurs). Comme structure de données, c'est une structure de
LIFO, c-à-d que la seule donnée accessible, c'est la dernière
qu'on y a mis. On peut se servir des représentations différentes
pour y arriver. Quand on parle du hardware, en revanche, c'est
prèsque toujours d'une région contigüe de la mémoire, adressée
par un régistre spécial, et avec des instructions spéciales qui
implémentent en effet la structure de données pile. (Mais ce
n'est qu'une première approximation. Sur un Sparc, on parle de
la pile des régistres, qui n'est pas dans la mémoire, mais qui
fonctionne aussi comme une pile.)

À proprement parler, ni l'une ni l'autre de ces définitions sont
connues du langage C++. (Dans la bibliothèque, en revanche, il y
a bien une classe templatée « adaptateur » std::stack, qui
implémente l'interface d'une pile à partir des structures de
données différentes.) Néaumoins...

Et donc, si j'ai bien tout compris à ce qu'on m'a enseigné,
une des raisons pour utiliser des pointeurs plutot que des
objets comme valeur de retour, c'est qu'au lieu de copier tout
l'objet sur la pile, on en copie que l'adresse...


On a une tendance en C++ à dire que les données à durée de vie
automatique sont « sur la pile », étant donné que dans les
implémentations courantes sur processeurs modernes, le
compilateur les met sur la pile hardware. C'est une commodité de
langage -- en fait, le langage définit un comportement, et je me
suis bien servi de C sur une machine sans pile hardware.

Maintenant, dans la pratique, quand le type d'un *paramètre*
(non une valeur de rétour) est trop grand, on en passe prèsque
toujours une copie sur la pile -- si on n'en passe qu'un
pointeur (ou une référence, qui au niveau de l'implémentation,
revient en général à un pointeur), il y a d'abord de fortes
chances qui'il passe dans un régistre, si l'implémentation
utilise des régistres pour des paramètres qui y tiennent, et
sinon, c'est en général moins cher de ne pusher qu'un seul
pointeur que de copier un objet grand et complex. Mais
j'insiste, ça va pour les paramètres, et non les valeurs de
retour.

En ce qui concerne les valeurs de retour, il y a deux solutions
adoptées, selon la taille et la sémantique de l'objet. Pour des
objets « simples » : des int ou des pointeurs, la valeur de
rétour se trouve dans un régistre. Je n'en connais pas
d'exception. Pour des valeurs grandes et complexes, la technique
la plus répandue, c'est que le code appelant alloue la mémoire
(sur la pile, parce que c'est un temporaire), et en passe
l'adresse à la fonction, qui construit l'objet là où on lui a
dit. Seulement, à cet égard, beaucoup d'optimisations sont
possibles, et ça arrive assez souvent qu'à la place de
construire un temporaire, on passe l'adresse finale où on veut
avoir l'objet, et il n'y a pas de copie supplémentaire.

La vraie différence entre les pointeurs ou les références, et
les valeurs, surtout en ce qui concerne les valeurs de rétour,
c'est la durée de vie. Chaque objet en C++ a sa propre durée de
vie, et un pointeur et une référence n'ont pas forcément la même
durée de vie que ce qu'il pointe. Dans le cas des paramètres, ce
n'est pas trop grave -- le langage spécifie qu'un temporaire
dure jusqu'à la fin de l'expression où il a été produit (au
moins), et s'il n'y a pas de temporaire, on passe un pointeur ou
une référence à un objet local, ou statique, qui a donc une
durée de vie supérieur à la fonction appelée. Il n'y a donc pas
de problème à utiliser le pointeur ou la référence, parce que
l'objet qu'il désigne existe bien. (Il peut y avoir de problème
si la fonction appelée stocke le pointeur ou la référence
ailleurs, et une fonction appelée plus tard s'en sert.) Pour
les valeurs de rétour, c'est plus délicat, parce que quand on
renvoie un pointeur ou une référence, on quitte la fonction qui
renvoie, et tous les objets locaux ou temporaires cessent
d'exister. Alors, qu'est-ce que le pointeur ou la référence
désigne ? S'il s'agit d'une fonction membre, et il renvoie un
pointeur ou une référence à une variable membre, pas trop de
problème, au moins dans l'immédiat, parce que l'objet continue
d'exister après l'appel de la fonction. De même dans les cas où
on renvoie un pointeur ou une référence à un élément contenu
dans une collection, ou à une variable statique. Mais si ce
qu'on renvoie est une valeur calculée (et donc créée) dans la
fonction même, on a un véritable problème, parce qu'en quittant
la fonction, l'objet désigné par le pointeur ou la référence
cesse d'exister. Dans la littérature anglo-saxone, c'est ce
qu'on appelle un « dangling pointer ». Et il a un comportement
indéfini -- ce qui veut dire, probablement pas ce qu'on veut,
mais peut-être ce qu'on veut lors de nos tests à nous, mais pas
ce qu'on veut lors des tests de reception chez le client.

La question que je me posais était de savoir ce qu'il se
passait dans le cas d'une référence au lieu d'un pointeur
comme valeur de retour d'une fonction.


En ce qui concerne les paramètres et les valeurs de rétour, les
références se comportent pratiquement comme des pointeurs.

Typiquement aussi, une référence s'implémente comme un
pointeur, dans les cas où le compilateur ne réussit pas à
l'éliminer complétement.


Dans ce cas, je supposes que tout se passe plus ou moins comme
si j'avais écrit:

Point* sommet() {return(&_p)}
puis dans mon programme :
point *pp=a.sommet();


Pas tout à fait. Ce que tu as écrit, c'était :

Point pp = a.sommet() ;

En renvoyant un pointeur, ça ne doit même pas compiler. En
renvoyant une référence, c'est à peu près comme si sommet()

Point pp = *a.sommet() ;

Si en revanche, sommet() renvoie un objet, ça serait une copie
de _p. Typiquement, le compilateur passera l'adresse où il veut
cette copie à sommet(), comme paramètre caché. Dans ce cas
précis, c'est une optimisation courante de passer directement
l'adresse de pp, plutôt que l'adresse d'un temporaire
quelconque. Mais ce n'est pas toujours le cas.

Reste que ta première considération doit être la durée de vie de
l'objet. In ne faut jamais, mais jamais renvoyer un pointeur ou
une référence à un objet qui cesse d'exister en quittant la
fonction. Dans la doute, on envoie l'objet. Une copie de plus
coûte un tout petit peu de temps CPU, qui est bien moins cher
que des heures ou des jours du temps ingenieur pour débugger un
comportement indéfini qui ne se manifeste que par intermittance.
N'oublie pas qu'avec ce que coûte deux jours d'un ingenieur, tu
te paies une machine tout neuve haut de gamme.

[..]
A a;
En supposant que tu es dans une fonction, ici `a' se

trouvera sur la pile. En dehors d'une fonction, il se
trouvera dans la mémoire statique.


Oui, autant pour moi, j'aurais du préciser que je plaçais la
partie du code non incluse dans la classe dans un bloc
main(){}...

p = a.sommet();
[..]


Enfin, le compilateur génèrera du code pour appeler
l'opérateur d'affectation de Point, avec l'adresse de p
comme premier paramètre, et la valeur renvoyée de l'appel de
sommet comme deuxième paramètre. Ici aussi, où il met les
paramètres dépend de l'API en question.


[..]
Si je vous comprend bien, le compilateur fait une différence
entre les valeurs de retour de type pointeur -remisées dans un
registre- et les autres : posées sur la pile ?


Au niveau de l'implémentation hardware, je crois qu'il n'y a
jamais de valeur renvoyée sur la pile. Quand une valeur de
retour ne passe pas dans un régistre, le compilateur génère un
paramètre supplémentaire pointeur.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
meow
Ok, les choses sont plus claires, merci.