Pointeurs, références & allocation dynamique Rappels sur les fonctions Les spécificités du C++ Structures et classes Encapsulation des données Notions de constructeur et de destructeur Fonctions et classes amies Surcharge d'opérateur Héritage Notions de patrons de fonctions et de classes Introduction à la librairie standard STL
Compilation et directives de préprocesseur Convention d'écriture et organisation des programmes Écriture/lecture sur l'entrée/sortie standard Les membres données statiques Utilisation de enum et de typedef
Retour menu principal

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

    complexe& 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;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 et delete

Notes :

1

on parle également de surcharge d’opérateur