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 21:44:42 -0800, "Vincent Lascaux" :

J'aurais du donner un autre nom à cette classe


Effectivement.

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


Cf mon message de 7h07.

Comme tout exemple minimaliste, cette fonction est d'une utilité douteuse...


Je ne te le fais pas dire. En fait, j'avoue n'avoir jamais rencontré
le problème dans une application réelle.

Mais il est concevable qu'en moyenne, le code ci-dessus soit plus rapide que
le code suivant


Va savoir... Qu'est-ce qui prouve qu'une décrémentation sera plus
rapide qu'une copie ?

Avatar
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.


Je veux dire "l'état de la variable après l'execution de la fonction est le
même qu'avant l'execution de la fonction"

Le problème le plus immédiat : s'assurer que toutes les opérations
"next()" sont réversibles en temps normal. [...]

Second problème : s'assurer que le contrat est bien respecté
en cas d'exception. [...]


Ces deux problemes sont là aussi avec la version non const de
nbElementsAfter puisqu'on s'attend là aussi à ce que l'itérateur ne soit pas
déplacé (même si le compilateur ne le sait pas lui).
Avoir une fonction qui ne remplit pas son contrat (ie un bug) c'est mal,
mais bon, ca arrive, et je vois pas pourquoi on devrait s'inquieter plus sur
ce bug que sur un autre (s'assurer que nbElementsAfter retourne bien la
bonne valeur est aussi assez difficile).
Pour la petite info, en mode debug, on utilise RAII pour prendre l'état de
l'objet à l'entrée de la fonction et vérifier à la sortie que c'est le même.

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


Je n'est pas à me soucier de probleme de multi threading pour le code sur
lequel je travaille, et je ne suis pas un expert dans ce domaine, mais si
c'était le cas, il est clair que nbElementsAfter devrait "locker"
l'itérateur comme pour le modifier dans le bloc de la fonction.

(et j'ai appris à mes dépens que sous Windows au moins, on travaille
parfois en multithread sans trop s'en rendre compte),


Tu peux donner quelques détails ? Je suis pas sur de comprendre...

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.


T'aurais un exemple ?

--
Vincent


Avatar
Vincent Lascaux
Qu'est-ce qui prouve qu'une décrémentation sera plus
rapide qu'une copie ?


Rien ne le prouve, mais j'ai en tête un operateur ++ qui fait quelque chose
comme (sur une représentation en base Base)

int i = 0;
while (i<val.size() && val[i] == Base-1)
val[i++] = 0;
if(i == val.size())
val.push_back(1);
else
val[i]++;

Si on prend un nombre aléatoire, la probabilité de modifier exactement n
chiffres est (Base-1)/Base^n (si je ne m'abuse). La moyenne est donc
(Base-1)*Sum(n/Base^n)
Pour un nombre de 100 chiffres, avec Base = 256, on va modifier en moyenne
moins de 1.004 élements, contre 100 pour une copie... Il y a forcément une
taille de nombre à partir de laquelle mon opérateur ++ est plus rapide
qu'une copie de l'intégralité du nombre.

--
Vincent

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

Ces deux problemes sont là aussi avec la version non const de
nbElementsAfter puisqu'on s'attend là aussi à ce que l'itérateur ne soit pas
déplacé (même si le compilateur ne le sait pas lui).


Sans doute. Mais j'essayais d'expliquer pourquoi le langage ne
contient rien pour indiquer que la valeur est la même à la fin et au
début.

(et j'ai appris à mes dépens que sous Windows au moins, on travaille
parfois en multithread sans trop s'en rendre compte),


Tu peux donner quelques détails ? Je suis pas sur de comprendre...


Par exemple, un code appelé en callback par un timer multimédia
s'exécute dans un thread différent de celui qui a créé le timer.


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

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.


T'aurais un exemple ?


J'avoue que je ne suis pas doué pour les exemples tordus. Je peux
toutefois te proposer ça :

Imagine une structure de données sous la forme de deux tableaux (par
exemple, deux std::vector<>). Un itérateur va parcourir le premier
tableau, puis le deuxième.

class iterator
{
private:
typedef ce_que_tu_veux Tableau;
Tableau& tableau1;
Tableau& tableau2;

iterator const& end_tableau_1;
iterator const& begin_tableau_2;

public:
Machin next();
int TailleDeuxiemeTableau() const;
};



Je suppose que la fonction nbElementsAfter est définie comme tu l'as
proposé :

int nbElementsAfter (iterator const& 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;
}


La fonction next() est simple : on incrémente quelque chose ; si on
est arrivé à la fin du premier tableau, on passe sur le deuxième.

Machin iterator::next()
{
incrementer();
if (*this == end_tableau_1) *this= begin_tableau_2;
return quelque_chose;
}

Passons maintenant à la fonction TailleDeuxiemeTableau(). Celle-là est
très simple : il suffit de calculer le nombre d'éléments entre le
début du deuxième tableau et la fin. Une ligne suffit :

int TailleDeuxiemeTableau() const;
{
return nbElementsAfter (end_tableau_1);
}


Avatar
Jean-Marc Bourguet
Fabien LE LEZ writes:

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.


Il me semble que c'est un comportement indéfini uniquement
dans le cas où l'iterateur passé a été déclaré const mais
pas dans les autres cas.

Donc avec

iterator i;

ça passe, avec

iterator const i;

non.

Vu que le const en C++ est plus logique que physique (voir
mutable par exemple), ça ne me gène pas trop tant qu'on est
sur que l'itérateur ne sera *jamais* modifié, même en cas
d'exceptions. Mais ce n'est pas pour autant que je le
ferai.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
kanze
Fabien LE LEZ wrote:

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


Ça se discute. L'utilisation de next() et de prev() est bien
plus compréhensible.

--
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
kanze
Fabien LE LEZ wrote:
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.


Pas du tout. Ce n'est un comportement indéfini que si
l'itérateur passé en paramètre est réelement const. Et définir
des itérateurs qui sont const, ça ne doit pas se produire
souvent.

Évidemment, ce n'est pas pour autant que c'est une bonne idée.
Le const donne bien un mauvais message à l'utilisateur.

--
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
kanze
Fabien LE LEZ wrote:
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."


D'où est-ce que tu tiens cette information ? Selon la norme
(§5.2.11/1), « Conversions that can be performed explicitly
using const_cast are listed below. »

Et ce que tu dis au compilateur avec const_cast, ce n'est pas
que tu ne vas pas modifier la variable, mais plutôt que l'objet
auquel le pointeur ou la référence réfère n'est pas en fait
const, bien que l'expression au départ contient un const. (Mais
le compilateur est obligé de prendre en compte ce fait de tout
façon en ce qui concerne les optimisations.)

Si tu romps cet engagement, i.e. si tu mens, le résultat est
indéfini.


Si l'objet auquel le pointeur ou la référence est réelement
const, et tu essaies de le modifier au moyen d'un const_cast, tu
as un comportement indéfini. Sinon, il n'y a pas de problème.

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.


Exactement. De même avec const_cast, tu affirme que tel pointeur
pointe en fait sur un objet qui n'est pas const.

Dans les deux cas, il y a des exceptions ; des cas qui sont
garantis à marcher même si tu mens. Donc, au moyen de
reinterpret_cast, tu peux régarder n'importe quel type comme si
c'était un tableau de unsigned char, sans comportement indéfini.
Et dans le cas de const_cast, tant que tu n'essaies pas de
modifier un objet qui est réelement const, tu as un comportement
on ne peut plus défini.

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


Il y a aussi de ça.


Sauf qu'il ne marche pas pour ça. Ce n'est pas parce qu'il y a
une référence à const que le compilateur peut supposer que la
valeur de l'objet ne change pas.

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.


Mais le problème ci-dessus, c'est que tu as essayé de modifier
un objet const, non le fait du const_cast. Si, par exemple, tu
écris :

int
main()
{
int n = 42 ;
int const& r1 = n ;
int& r2 = const_cast< int& >( r1 ) ;
r2 = 0 ;
std::cout << n << std::endl ;
return 0 ;
}

Tu as un comportement bien défini, et tout compilateur conforme
sortira 0.

--
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
kanze
Vincent Lascaux wrote:
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 :)


Dans mon propre code, je suis la convention que les noms de
types commencent toujours par une majuscule. Du coup, j'ai des
Iterator, qui sont des itérateurs, et des « iterator », qui sont
les batards à la STL. (En fait, j'essaie assez souvent à
supporter les deux interfaces.)

--
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


1 2 3 4 5