OVH Cloud OVH Cloud

Sens de const

64 réponses
Avatar
Vincent Lascaux
Bonjour,

Je voudrais savoir quel sens vous mettez derriere le mot clé "const", ou
plutot quel contrat une fonction qui a des parametres const doit remplir.

Pour être plus clair, voici le cas qui m'interesse plus particulierement :
nous avons une classe "iterator" qui me permet d'itérer sur une structure de
donnée. Un "iterator" a de bonnes raisons (qui ne sont pas discutables :))
d'être un peu lourd à copier. Maintenant imaginons une fonction
"nbElementsAfter" qui fait ca :

int nbElementsAfter(iterator& it)
{
int nb = 0;
while(it.next()) nb++;
for(int i=0; i<nb; i++) it.prev();
return nb;
}

Est-ce que cette fonction doit prendre un iterator& ou un const iterator& ?
Lequel de ces contrats vous semble le plus sensé pour une fonction qui prend
un const iterator& : "je ne modifierai pas l'iterateur" ou "l'iterateur
pointera au même endroit avant et apres l'appel" (ou encore autre chose ?) ?

Merci

--
Vincent

10 réponses

1 2 3 4 5
Avatar
Fabien LE LEZ
On Sat, 7 Jan 2006 19:10:46 -0800, "Vincent Lascaux" :

int nbElementsAfter(iterator& it)
{
int nb = 0;
while(it.next()) nb++;


J'imagine que "next()" est une fonction membre non-const, i.e. elle
modifie "it". Par conséquent, "it" ne peut pas être const. Et, comme
on n'a pas le choix, la discussion s'arrête là.

Au fait, si tu appelles ton objet "iterator", il vaudrait mieux lui
mettre des opérateurs ++ et -- au lieu de next() et prev().

Avatar
Fabien LE LEZ
On Sat, 7 Jan 2006 19:10:46 -0800, "Vincent Lascaux" ::

Est-ce que cette fonction doit prendre un iterator& ou un const iterator& ?


Accessoirement, je te conseillerais d'écrire "iterator const&", il y a
moins de chance de confondre avec "const_iterator&".

Avatar
Vincent Lascaux
int nbElementsAfter(iterator& it)
{
int nb = 0;
while(it.next()) nb++;


J'imagine que "next()" est une fonction membre non-const, i.e. elle
modifie "it". Par conséquent, "it" ne peut pas être const. Et, comme
on n'a pas le choix, la discussion s'arrête là.


int nbElementsAfter(const iterator& const_it)
{
iterator& it = const_cast<iterator&>(const_it);

int nb = 0;
while(it.next()) nb++;
for(int i=0; i<nb; i++) it.prev();
return nb;
}

Au fait, si tu appelles ton objet "iterator", il vaudrait mieux lui
mettre des opérateurs ++ et -- au lieu de next() et prev().


En fait la classe est plus compliquée, on a plusieurs façons de déplacer
l'itérateur... Je voulais juste avoir un exemple simple (en fait il n'y a
pas non plus de nbElementsAfter, mais une fonction qui utilise l'iterateur
pour lire un certains nombres d'élements et le remet à sa place.

--
Vincent


Avatar
Fabien LE LEZ
On Sat, 7 Jan 2006 20:10:45 -0800, "Vincent Lascaux"
:

int nbElementsAfter(const iterator& const_it)
{
iterator& it = const_cast<iterator&>(const_it);

int nb = 0;
while(it.next()) nb++;


C'est un comportement indéfini.
const_cast<> sert à peu près uniquement à appeler une fonction qui ne
modifie pas l'objet, mais déclarée non-const par erreur.
iterator::next() ne répond pas à cette définition (puisqu'elle modifie
l'objet), donc il s'agit ici d'un abus de const_cast<>.

Le code que tu proposes n'est pas valide ; il ne fait donc pas partie
des possibilités.

Avatar
Vincent Lascaux
int nbElementsAfter(const iterator& const_it)
{
iterator& it = const_cast<iterator&>(const_it);

int nb = 0;
while(it.next()) nb++;


C'est un comportement indéfini.


Ah bon ? Je trouve personnellement que c'est dommage qu'on ne puisse pas
utiliser const dans ce cas. Du coup il y a pas mal de fonctions qui ne
comportent plus de const dans leur signature parcequ'elles ont besoin
d'appeler des fonctions comme nbElementsAfter. Si on pouvait avoir "int
nbElementsAfter(const iterator&)" comme signature, on pourrait avoir des
const dans les parametres des fonctions qui l'appelle et avoir des
vérifications par le compilateur que la fonction retourne l'iterateur dans
l'état dans lequel elle l'a trouvé.

Quelle est la raison de cette interdiction de faire des const_cast ? C'est
pour laisser plus de liberté d'optimisation au compilateur ? Je croyais
qu'il n'avait pas le droit de faire d'optimisation basée sur la "constness"
d'une expression sans avoir vérifié qu'il n'y avait pas de const_cast dans
les fonctions appelées...

--
Vincent


Avatar
Fabien LE LEZ
On Sat, 7 Jan 2006 21:11:04 -0800, "Vincent Lascaux" :

Quelle est la raison de cette interdiction de faire des const_cast ?


Parce que const_cast<> n'est pas un opérateur de conversion à
proprement parler, mais un moyen de dire au compilateur "Je soussigné
le programmeur, m'engage à ce que tel code ne modifie pas telle
variable, même si toi, le compilateur, tu ne peux pas t'en rendre
compte."
Si tu romps cet engagement, i.e. si tu mens, le résultat est indéfini.

C'est un peu la même chose pour reinterpret_cast<> : tu peux t'en
servir pour dire "J'affirme que tel pointeur pointe en fait sur une
variable de tel type.", mais si c'est un mensonge, le résultat est
indéfini.

C'est pour laisser plus de liberté d'optimisation au compilateur ?


Il y a aussi de ça.

int main()
{
int const n= 42;
int& ptr_n= const_cast<int>(n);
ptr_n= 0;
cout << n << endl;
}

Le programme ci-dessus a un comportement indéfini : il peut afficher à
peu près n'importe quoi. En pratique, il y a de fortes chances pour
qu'il affiche 0 ou 42, suivant le compilo et les options de
compilation.

Avatar
Fabien LE LEZ
On Sat, 7 Jan 2006 19:10:46 -0800, "Vincent Lascaux" :

Un "iterator" a de bonnes raisons (qui ne sont pas discutables :))
d'être un peu lourd à copier.


Note que tu pars d'emblée sur de mauvaises bases : un itérateur au
sens de la STL ("iterator") est un objet très souvent copié. On ne le
passe quasiment jamais par référence const.

Avatar
Vincent Lascaux
"Je soussigné
le programmeur, m'engage à ce que tel code ne modifie pas telle
variable, même si toi, le compilateur, tu ne peux pas t'en rendre
compte."


Je voudrais dire "Je soussigné le programmeur m'engage à ce qu'apres
l'execution de la fonction, la variable aura toujours la même valeur" (au
sens de const, ie sauf mutables).
Il n'y a donc pas moyen de faire ca ? C'est dommage parceque j'ai le
sentiment que c'est ce qui est important. En tant qu'utilisateur de
nbElementsAfter, ce qui m'importe c'est que la fonction me retourne la bonne
valeur et que je puisse continuer à travailler avec mon itérateur. Le fait
qu'en interne elle modifie temporairement l'iterateur ne m'interesse pas
vraiment. J'ai du mal à voir un cas où il est important d'avoir dans le
contrat d'une fonction "ne modifie pas l'objet" plutot que le moins fort
"retourne l'objet dans l'état d'origine". Const impose une contrainte à la
fonction qui me semble trop forte et me limite dans l'implémentation de
nbElementsAfter sans que j'ai l'impression que ca n'apporte quoi que ce soit
à l'appelant.

--
Vincent

Avatar
Vincent Lascaux
Note que tu pars d'emblée sur de mauvaises bases : un itérateur au
sens de la STL ("iterator") est un objet très souvent copié. On ne le
passe quasiment jamais par référence const.


Et qui dit que je fais un "iterator" au sens de la STL ? J'aurais du donner
un autre nom à cette classe :)
De toutes façons, le problème n'est pas là... C'est pas très difficile de
trouver d'autres exemples. Pour une classe d'entiers long, utilisée pour des
entiers de plusieurs ko par exemple,

void printNext(LongInt& val)
{
++val;
std::cout << val;
--val;
}

Comme tout exemple minimaliste, cette fonction est d'une utilité douteuse...
Mais il est concevable qu'en moyenne, le code ci-dessus soit plus rapide que
le code suivant (qui peut en plus poser des problemes de mémoire à cause de
la création du temporaire "val+1")

void printNext(const LongInt& val)
{
std::cout << val + 1;
}

C'est cher payer pour conserver le const...

--
Vincent

Avatar
Fabien LE LEZ
On Sat, 7 Jan 2006 21:35:47 -0800, "Vincent Lascaux" :

Je voudrais dire "Je soussigné le programmeur m'engage à ce qu'apres
l'execution de la fonction, la variable aura toujours la même valeur"


Je comprends l'idée, mais la formulation me paraît imprécise.
Méfie-toi des mots comme "toujours" qui ont plusieurs sens légèrement
différents.

Car un des problèmes est dans cette subtilité : l'objet n'a pas
toujours la même valeur ; à un moment de l'exécution de la fonction,
il a une valeur différente.

Le problème le plus immédiat : s'assurer que toutes les opérations
"next()" sont réversibles en temps normal. Ça demande une connaissance
du fonctionnement de iterator, ou au moins un contrat assez
restrictif. AMHA, nbElementsAfter serait mieux comme fonction membre.

Second problème : s'assurer que le contrat ("La valeur est la même à
la fin de la fonction") est bien respecté en cas d'exception.
Si next() et prev() sont déclarées "nothrow", tout va bien.
Sinon, je ne sais pas comment tu comptes t'en sortir.

Troisième problème : s'assurer que pendant l'exécution de la fonction,
aucun code ne s'attend à voir la valeur de départ dans l'objet.
Bien évidemment, le cas du multithread vient tout de suite à l'esprit
(et j'ai appris à mes dépens que sous Windows au moins, on travaille
parfois en multithread sans trop s'en rendre compte), mais il est
possible d'imaginer des cas où le code même de next() (ou de prev())
s'attend à ce que l'objet passé en argument ait sa valeur de départ.


Pour résoudre ton problème, je ne vois pas vraiment d'autre solution
que la copie, sinon de l'objet "iterator", au moins d'une partie de
cet objet : extraire de l'objet "iterator" un sous-objet copiable et
contenant assez d'information pour calculer la réponse.


7h07 : une heure à pleurer.

1 2 3 4 5