Surdéfinition d’opérateur
Le C++ permet de surdéfinir les opérateurs1 c’est-à-dire de donner un sens
différent à un symbole selon le contexte. Cette possibilité tient au fait que
les opérateurs ne se différencient des fonctions que syntaxiquement, pas
logiquement. À ce titre, le compilateur traite un appel à un opérateur comme un
appel à une fonction. On pourra donc surdéfinir ou surcharger un opérateur dès
lors que la nouvelle définition se différenciera, sans ambiguité, des
précédentes. Par exemple, surdéfinir l’opérateur +
permet que l’addition
i.e. le symbole +
binaire, n’ait pas le même sens s’il agit sur deux entiers
ou si l’addition porte sur deux objets de la classe point
.
Surcharge des opérateurs internes
Une première méthode pour surcharger les opérateurs consiste à les considérer
comme des méthodes de la classe sur laquelle ils s’appliquent. Le nom de ces
méthodes est donné par le mot clé operator
, suivi de l’opérateur à
surcharger. Le type de la fonction est le type du résultat donné par
l’opération, et les paramètres, donnés en argument, sont les opérandes. La
syntaxe est la suivante :
type operatorOp(argument)
L’écriture A Op B
où Op
est, par exemple, +,
,*==… se traduit par
A.operatorOp(B)
Le premier opérande est toujours l’objet auquel cette fonction s’applique. Cette
manière de surcharger les opérateurs est donc particulièrement adaptée pour les
opérateurs qui modifient l’objet sur lequel ils travaillent tels que les
opérateurs =, +
, ++=…. Par ailleurs, les opérateurs (sur)définis en
interne devront souvent renvoyer l’objet sur lequel ils travaillent (ce n’est
pas une nécessité cependant). On utilisera alors le pointeur this
qui pointe
sur l’adresse de l’objet qui a appelé la méthode de surdéfinition.
Afin d’illustrer les propriétés définies dans ce paragraphe, nous proposons
quelques exemples d’implémentation de surdéfinition d’opérateurs en relation
avec la classe complexe
class complexe { public: complexe(const double reel, const double img); complexe & operator+=(const complexe& source); complexe & operator*=(const complexe& source); ... private: double m_reel; double m_imaginaire; }; complexe::complexe(const double reel, const double img) : m_reel(reel), m_imaginaire(img) {} complexe & complexe::operator+=(const complexe & source) { m_reel += source.m_reel; m_imaginaire += source.m_imaginaire; return *this; } complexe & complexe::operator*=(const complexe & source) { const double tmp = m_reel*source.m_reel - m_imaginaire*source.m_imaginaire; m_imaginaire = m_reel*source.m_imaginaire + m_imaginaire*source.m_reel; m_reel = tmp; return *this; }
Comme pour toute surdéfinition de fonction, le compilateur choisit, selon le
contexte, quelle surcharge d’opérateur utiliser. Ainsi, on pourra, selon les
besoins, surcharger l’opérateur +=
de l’exemple précédent en fournissant en
argument non plus un objet Complexe
sinon un entier ou un double. La
définition de la méthode sera
complexe& operator+=(const double& a);
Surcharge des opérateurs en externe
La définition de l’opérateur ne se fait plus dans la classe qui l’utilise, mais
en dehors de celle-ci par surcharge d’un opérateur de l’espace de
nommage. L’opérateur surdéfini est déclaré comme une fonction travaillant avec
la classe dont l’opérateur doit être surchargé. Pour que cette fonction puisse
accéder aux membres de la classe, elle est généralement définie comme fonction
amie (friend
). Le prototype est le suivant
friend type operatorOp(argument);
À titre d’exemple, on pourra surcharger l’opérateur +
de la classe complexe
complexe operator+(const complexe & z1, const complexe & z2) const { return complexe(z1.m_reel + z2.m_reel, z1.m_imaginaire + z2.m_imaginaire); }
On notera bien que la surcharge de l’opérateur est extérieure à la classe
complexe
: sa déclaration ne fait pas intervenir l’opérateur de portée
::
. Par ailleurs, la déclaration précédente suppose que la construction
complexe(const double, const double)
est envisageable.
L’avantage de cette syntaxe est que l’opérateur est réellement symétrique, contrairement au cas où les opérateurs sont définis à l’intérieur de la classe.
Remarques importantes
- Lorsque l’on surdéfinit un opérateur, c’est à priori pour l’utiliser à
plusieurs reprises. Il faut donc apporter un soin particulier à l’optimisation
du code. En conséquence, il est important de savoir si l’on déclare ou non la
fonction
inline
Dans le cas d’opérateur unaire tel que l’opérateur
-()
qui effectue la transformation \(z\mapsto-z\), la (sur)définition de la fonction ne prend pas d’argument. On écrit alorscomplexe& operator-() const { return complexe(-m_reel, -m_imaginaire); }
- De manière générale, si une méthode ne modifie pas les membres de la classe,
on ajoute à la fin de sa définition, le mot clé
const
(cf. exemple précédent). Cette remarque prend toute son importance lorsque l’on manipule les opérateurs permettant ainsi de s’assurer que les membres ne sont pas modifiés de manière inopportune.
Constructeur de recopie et opérateur d’affectation =
L’opérateur d’affectation =
peut lui aussi être redéfini. Cependant, son rôle
peut parfois interférer avec celui du constructeur de recopie. De même que le
constructeur est la fonction appelée lors de la création d’un objet, le
constructeur de recopie est appelé lors de la copie d’un objet vers un autre
objet du même type (e.g. une instruction du type z1 = z2;
où z1
et z2
sont
des instances de la classe complexe
).
La définition du constructeur de recopie est voisine de celle du constructeur par défaut sachant toutefois que le constructeur de recopie possède comme argument une référence vers la classe. Ainsi, son prototype s’écrit:
nom_classe(nom_classe &);
tandis que sa déclaration est
nom_classe::nom_classe(nom_classe & objet_de_type_nom_classe);
Exemple:
complexe::complexe(const complexe & source) { m_reel = source.m_reel; m_imaginaire = source.m_imaginaire; }
L’opérateur d’affectation =
se définit comme toute surcharge d’opérateur et sa
déclaration devient
complexe& complexe::operator=(const complexe & source) { if (&source != this) { m_reel = source.m_reel; m_imaginaire = source.m_imaginaire; } return *this; }
Du point de vue de la syntaxe, la surcharge d’opérateur d’affectation est voisine de celle du constructeur de recopie. Néanmoins, la surcharge de l’opérateur d’affectation signale bien souvent que la classe n’a pas une structure simple (présence d’un pointeur en particulier) et qu’en conséquence, le constructeur de recopie et le destructeur par défaut, fournis par le compilateur, ne suffisent pas. Il faut donc veiller à respecter la règle des trois, qui stipule que si l’une des ces méthodes est redéfinie, il faut que les trois le soient. Par ailleurs, si le constructeur de recopie n’est pas redéfini, les écritures telles que :
classe objet = source;
ne fonctionnent pas correctement. En effet, c’est le constructeur de recopie qui est appelé dans ce cas, et non l’opérateur d’affectation comme le suggère la syntaxe.
Un autre problème important tient à l’autoaffectation. Non seulement affecter un
objet à lui-même est inutile et consommateur de ressources, mais de plus, cela
peut s’avérer dangereux : l’affectation risque de détruire les données membres
de l’objet avant même qu’elles ne soient copiées, ce qui provoque au final ni
plus ni moins que la destruction de l’objet. Une solution simple présentée dans
l’exemple précédent consiste à ajouter un test sur l’objet source en début de
surcharge d’opérateur : if (&source !=
this)
.
Pour toutes ces raisons, la surcharge de l’opérateur d’affectation s’avère une opération souvent délicate. Dans la grande majorité des cas, on évitera de surcharger l’opérateur d’affectation en utilisant le constructeur de recopie par défaut.
Remarques générales
- l’opérateur
()
est intéressant car il est “n-unaire”. - il est aussi possible de surdéfinir :
- les opérateurs de transtypage (ou de casting)
- les opérateurs de déférencement
*
et d’indirection&
new
etdelete
Notes :
on parle également de surcharge d’opérateur