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

const et surcharges

5 réponses
Avatar
Sylvain
Bonjour,

j'ai découvert récemment que je me trompais sur un point.
Je pensais que lorsqu'on surchargeait une méthode en créant une const et une
non const, le compilateur choisissait toujours la const s'il pouvait et la
non const sinon.
Par ex :

class A {
T &data() { return d ;}
const T &data() const { return d ;}

T d ;
}

Au contraire, il choisit toujours la méthode non const, sauf s'il n'a pas le
choix.
Ex (imaginons que T a une méthode print() const)

A a ;

a.data().print() ; // il appelle la non const

const A &aa = a ;
aa.data().print() ; // il appelle la const


J'utilise g++ 4.0.1, est-ce un bug de ce compilateur ? Est-ce définit dans
la norme comme ça ? Est-ce par difficulté d'implémentation que ce
comportement existe ?

Ca ne parait pas génant, mais pour moi c'est très génant car je veux un
comportement différent dans le cas non const, car ça veut dire que l'objet
d va être modifié.

Merci pour tout éclairage, et merci encore plus si quelqu'un a une
solution :-).

5 réponses

Avatar
Jean-Marc Bourguet
Sylvain writes:

j'ai découvert récemment que je me trompais sur un point.
Je pensais que lorsqu'on surchargeait une méthode en
créant une const et une non const, le compilateur
choisissait toujours la const s'il pouvait et la non const
sinon.


Non. Les membres const sont utilisés pour les objets const,
les membres non const pour les objets non const. La manière
dont est utilisé un résultat n'intervient jamais dans la
résolution de surcharge.

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
Sylvain wrote:

j'ai découvert récemment que je me trompais sur un point.
Je pensais que lorsqu'on surchargeait une méthode en créant
une const et une non const, le compilateur choisissait
toujours la const s'il pouvait et la non const sinon. Par
ex :

class A {
T &data() { return d ;}
const T &data() const { return d ;}

T d ;
}

Au contraire, il choisit toujours la méthode non const, sauf
s'il n'a pas le choix.


Les règles de la résolution du surcharge ne sont pas aussi
simple que ça. Mais en gros, à quelques exceptions près, la
résolution ne dépend que de l'expression d'appel, sans tenir
compte de la contexte où elle apparaît. Il y a de bonnes raisons
pour ça, aussi bien par rapport au compilateur que par rapport à
celui qui doit lire le programme. En suite, une conversion T& en
T const& est justement ça : une conversion ; s'il existe une
correspondance exacte, on le préfère. Et on traite l'objet sur
lequel on appelle la fonction comme un paramètre caché, de type
A& (s'il s'agit d'une fonction non const) ou A const& (pour une
fonction const).

Ex (imaginons que T a une méthode print() const)

A a ;

a.data().print() ; // il appelle la non const


Tout à fait. On considère l'expression a.data(), en isolation.
On le traite comme si les signatures étaient : data( A& ) et
data( A const& ), et l'appel data( a ). Alors, la première
correspond exactement, et la deuxième a besoin d'une conversion
A& en A const&. On choisit donc la correspondance exacte. (Mais
évidemment, ce n'est pas toujours aussi simple.)

const A &aa = a ;
aa.data().print() ; // il appelle la const


Que veux-tu qu'il fasse d'autre ? La première démarche dans la
résolution du surcharge, c'est de créer l'ensemble de fonctions
qui peuvent être appelées. Ici, il n'y en est qu'une ; c'est
donc forcément elle qui sera choisie.

J'utilise g++ 4.0.1, est-ce un bug de ce compilateur ?


Non.

Est-ce définit dans la norme comme ça ?


Tout à fait.

Est-ce par difficulté d'implémentation que ce comportement
existe ?


Difficulté d'implémentation en partie seulement.
Conceptuellement, il y a de grands avantages en ce que la
sémantique d'une expression ne dépend pas de la contexte où il
apparaît. Surtout dans le cas d'un langage comme C++, avec des
conversions implicites en pagaille et où on peut même ignorer la
valeur de rétour.

Ca ne parait pas génant, mais pour moi c'est très génant car
je veux un comportement différent dans le cas non const, car
ça veut dire que l'objet d va être modifié.

Merci pour tout éclairage, et merci encore plus si quelqu'un a
une solution :-).


La solution habituelle est de se servir d'un objet Proxy comme
valeur de rétour de data(). Typiquement, il s'agit de distinguer
l'affectation des conversions lvalue en rvalue ; le Proxy
implément l'opérateur d'affectation pour faire les modifications
voulues dans l'objet de base, et un opérateur de conversion pour
la conversion lvalue en rvalue. Quelque chose du genre :

class TProxy
{
public:
TProxy( T& owner ) : myOwner( &owner ) {}
void operator=( T const& other ) const
{
myOwner->assign( other ) ;
}
operator T() const
{
return *myOwner ;
}
private:
T* myOwner ;
} ;

(Note la signature assez original de l'opérateur d'affectation.)

Dans le cas où il s'agit d'appeler des fonctions membres sur
l'objet, c'est plus complexe, parce qu'on ne peut pas surcharger
l'opérateur.() (et même si on pouvait, on aurait encore le
problème de distinguer entre les utilisations const et
non-const). Pour un cas particulier, comme ici, on pourrait bien
implémenter les fonctions de renvoi dans le Proxy, pour chaque
fonction voulue, mais c'est une solution assez fragile.

--
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
Casillux
Merci beaucoup pour cette réponse précise et détaillée.
J'avoue quand même que je n'ai pas tout compris sur le principe de la
classe Proxy.

la class A sera donc
class A {
ProxyT &data() { return d ;}
const ProxyT &data() const { return d ;}

T d ;
}

C'est ça ?

J'avoue que je n'ai pas compris la subtilité avec l'operateur On l'utilise toujours
data().machin = 2 ? si machin est un champ de T ?
Avatar
kanze
Casillux wrote:
Merci beaucoup pour cette réponse précise et détaillée.
J'avoue quand même que je n'ai pas tout compris sur le
principe de la classe Proxy.

la class A sera donc
class A {
ProxyT &data() { return d ;}
const ProxyT &data() const { return d ;}

T d ;
}

C'est ça ?


Non.

Ce que tu veux, c'est déterminer la fonction appelée selon
l'utilisation de sa valeur de rétour. Ce que le C++ ne supporte
pas directement. L'idée derrière le Proxy, c'est que la fonction
renvoie un objet (non une référence) d'un type temporaire, le
proxy. Ensuite, ce qu'on fait sur la valeur de rétour se résoud
en appels à des fonctions (surcharges d'opérateurs, conversions
implicites, etc.) sur le proxy. Le compilateur choisit donc la
fonction voulue sur le proxy, et ces fonctions appellent des
fonctions différentes sur l'objet initial.

Un exemple simple serait un tableau qui garde en fait ces
éléments dans un fichier disque. Quand on modifie un élément, il
faut l'écrire sur disque. Dans une première version, on
l'implémente avec deux fonctions, set(indice,nouvelleValeur) et
get(indice). Jusqu'à là, aucun problème, set écrit sur disque,
et get lit. Maintenant, on veut lui donner l'interface classique
d'un tableau, avec un operator[]. Seulement, qu'on écrit ou
qu'on lit dépend non de la const-ness de l'objet, mais de ce
qu'on fait avec la valeur de rétour de cet opérateur. Alors, on
utilise un proxy :

class DiskVector
{
public:
class Proxy
{
friend class DiskVector ;
Proxy( DiskVector* owner, IndexType index )
: myOwner( owner )
, myIndex( index )
{
}
public:
operator ValueType() const
{
return myOwner->get( myIndex ) ;
}
void operator=( ValueType const& other ) const
{
myOwner->set( myIndex, other ) ;
}

private:
DiskVector* myOwner ;
IndexType myIndex ;
} ;

Proxy operator[]( IndexType index )
{
return Proxy( this, index ) ;
}
ValueType operator[]( IndexType index ) const
{
return get( index ) ;
}

// ...
} ;

(Parfois il faut deux proxy, un pour l'opérateur const, et
l'autre pour l'opérateur non-const. Ici, le proxy sur const ne
supporterait qu'une seule opération, alors autant le faire
directement dans la fonction, sans proxy.)

Maintenant, considérons les utilisations (vect est une instance
de DiskVector) :

ValueType var ;
var = vect[ indice ] ;
vect[ indice ] = var ;

Dans le premier, à droit de l'affectation, j'ai besoin d'un
rvalue de type ValueType. Mais le compilateur a un
DiskVector::Proxy. Il cherche donc à le convertir en ValueType.
Ce qu'il peut faire en appelant l'opérateur de conversion, ce
qui mène à un appel à DiskVector::get, et on lit la valeur du
disque.

Dans le deuxième, à gauche de l'affectation, on a un
DiskVector::Proxy, et à droit un ValueType. Le compilateur
cherche donc un opérateur d'affectation qui prend ces deux
types. Qu'il trouve, dans DiskVector::Proxy. Il l'appelle,
alors, ce qui mène à un appel à DiskVector::put.

J'avoue que je n'ai pas compris la subtilité avec l'operateur =
On l'utilise toujours
data().machin = 2 ? si machin est un champ de T ?


Assez peu, en fait.

Si tu veux accèder à des membres de l'élément (que ces membres
soient des données ou des fonctions), et que le choix du membre
dépend de ce qu'on fait avec sa valeur de rétour, ça va être
assez complex. Il faudrait que data() renvoie un proxy, que
ce proxy contient les membres avec les mêmes noms que ceux de
l'objet même, et que le type de chacun des ces membres soit un
proxy comme ci-dessus. Dans ce cas-ci, donc, que data() renvoie
un ProxyT, que le ProxyT contient un membre public machin, et
que le type de machin soit un proxy comme ci-dessus, qui
supporte la conversion et l'affectation.

C'est lourd, et c'est fragile. Franchement, je chercherais une
autre solution. En gros, avec un proxy, on peut facilement
distinguer entre l'affectation et la conversion en rvalue, en
surchargeant les opérateurs correspondants. Pour en faire plus,
il faut plus ou moin émuler l'interface (la partie publiquement
accessible) de l'objet, ce qui devient vite très lourd, et qui
exige qu'on suit soigneusement toutes les évolutions dans le
type de l'objet de base. Je ne dis pas ne jamais le faire ; ça
pourrait se justifier, par exemple, dans le cas où le type de
base était un complex donne les parties réeles et imaginaires
étaient directement accessibles. Mais de tels cas sont à mon
avis assez rares.

--
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
Casillux
Génial merci !
Cet exemple est très clair.
J'ai maintenant tous les éléments pour faire exactement ce que je voulais.