Accueil
Accueil Le Club Delphi Kylix C C++ Java J2EE DotNET & C# Visual Basic Access Pascal Dev Web PHP ASP XML UML SQLSGBD Windows Linux Autres
logo

precedent    sommaire    suivant   


Qu'est ce que la surcharge d'opérateur ?
auteur : Marshall Cline
Cela permet de fournir une façon intuitive d'utiliser les interfaces de vos classes aux utilisateurs. De plus, cela permet aux templates de travailler de la même façon avec les classes et les types de base.

La surcharge d'opérateur permet aux opérateurs du C++ d'avoir une signification spécifique quand ils sont appliqués à des types spécifiques. Les opérateurs surchargés sont un "sucre syntaxique" pour l'appel des fonctions :

class Fred {
   public:
     ...
};
 
 #if 0
 
   // Without operator overloading:
   Fred add(const Fred& x, const Fred& y);
   Fred mul(const Fred& x, const Fred& y);
 
   Fred f(const Fred& a, const Fred& b, const Fred& c)
   {
     return add(add(mul(a,b), mul(b,c)), mul(c,a));    // Yuk...
   }
 
 #else
 
   // With operator overloading:
   Fred operator+ (const Fred& x, const Fred& y);
   Fred operator* (const Fred& x, const Fred& y);
 
   Fred f(const Fred& a, const Fred& b, const Fred& c)
   {
     return a*b + b*c + c*a;
   }
 
 #endif

Quel est l'avantage de surcharger un opérateur ?
auteur : Marshall Cline
Surcharger les opérateurs standards permet de tirer parti de l'intuition des utilisateurs de la classe. L'utilisateur va en effet pouvoir écrire son code en s'exprimant dans le langage du domaine plutôt que dans celui de la machine.

Le but ultime est de diminuer à la fois le temps d'apprentissage et le nombre de bugs.


Quelques exemples de surcharge d'opérateur
auteur : Marshall Cline
Parmi les nombreux exemples que l'on pourrait citer :
  • myString + yourString pourrait servir à concaténer deux objets string
  • myDate++ pourrait servir à incrémenter un objet Date
  • a * b pourrait servir à multiplier deux objets Number
  • a[ i ] pourrait donner accès à un élément contenu dans un objet Array
  • x = *p pourrait déréférencer un "pointeur intelligent" qui "pointerait" en fait sur un enregistrement sur disque — le déréférencement irait chercher l'enregistrement sur le disque, le lirait, et le stockerait dans x.


La surcharge d'opérateur n'embellit pas vraiment ma classe ; ce n'est pas censé rendre le code plus lisible?
auteur : Marshall Cline
La surcharge d'opérateur facilite la vie des utilisateurs d'une classe, mais pas celle du développeur de la classe !

Prenez l'exemple suivant :

class Array {
    public:
      int& operator[] (unsigned i);     // Certains n'aiment pas cette syntaxe
      // ...
};

inline int& Array::operator[] (unsigned i) // Certains n'aiment pas cette syntaxe
{
      // ...
}
Certains programmeurs n'aiment pas le mot-clé operator ni la syntaxe quelque peu bizarre que l'on doit utiliser dans le corps même de la classe. La surcharge d'opérateur n'est pas faite pour faciliter la vie du développeur de la classe, mais est faite pour faciliter la vie de l'utilisateur de la classe :

int main()
{
    Array a;
    a[3] = 4;   // Le code utilisateur doit être facile à écrire et à comprendre...
    // ...
}
Souvenez que dans un monde orienté réutilisation, vos classes ont des chances d'être utilisées par de nombreux programmeurs alors que leur construction incombe à vous et à vous seul. Donc, favorisez le plus grand nombre même si ça rend votre tâche plus difficile.


Quels opérateurs peut-on ou ne peut-on pas surcharger ?
auteur : Marshall Cline
La plupart des opérateurs peuvent être surchargés. Les seuls opérateurs C que l'on ne peut pas surcharger sont . et ?: (et aussi sizeof, qui techniquement est un opérateur). C++ vient avec quelques opérateurs supplémentaires, dont la plupart peuvent être surchargés à l'exception de ::, typeid et de .*

Voici un exemple de surcharge de l'opérateur d'indexation (qui renvoie une référence). Tout d'abord, sans surcharge :

class Array {
   public:
      int& elem(unsigned i)        
      { 
          if (i > 99) 
              error(); 
          return data[i]; 
      }
   private:
      int data[100];
};

int main()
{
    Array a;
    a.elem(10) = 42;
    a.elem(12) += a.elem(13);
    // ...
}
Le même exemple, cette fois-ci avec la surcharge :

class Array {
    public:
      int& operator[] (unsigned i) 
      { 
          if (i > 99) 
              error(); 
          return data[i]; 
      }
    private:
      int data[100];
};

int main()
{
    Array a;
    a[10] = 42;
    a[12] += a[13];
}

Peut-on surcharger operator== de façon qu'il compare deux char[] en faisant une comparaison de chaîne ?
auteur : Marshall Cline
Non, car au moins l'un des deux opérandes d'un opérateur surchargé doit être d'un type utilisateur (c'est-à-dire une class dans la majorité des cas).
Et même si C++ permettait cela (il ne le permet pas), vous auriez tout intérêt à utiliser la classe string qui est bien plus adaptée qu'un tableau de caractères.


Peut-on définir un operator** qui calcule "x à la puissance y" ?
auteur : Marshall Cline
Non.

Le nom, la précédence, l'associativité et l'arité (le nombre d'opérandes) d'un opérateur sont fixés par le langage. Et C++ n'ayant pas d'operator**, une classe ne peut à fortiori pas en avoir.

Si vous en doutez, sachez que x ** y est en fait équivalent à x * (*y) (le compilateur considère que y est un pointeur). En outre, la surcharge d'opérateur est juste un sucre syntaxique qui est là pour remplacer avantageusement les appels de fonction. Et ce sucre syntaxique, même s'il est bien utile, n'apporte rien de fondamental. Dans le cas qui nous intéresse ici, je vous suggère de surcharger la fonction pow(base,exposant) (<cmath> contient une version double précision de cette fonction).

Notez en passant que l'operator^ pourrait faire l'affaire pour "x à la puissance y", à ceci près qu'il n'a ni la bonne précédence ni la bonne associativité.


Comment implémenter un opérateur d'indexation pour une classe Matrix ?
auteur : Marshall Cline
Utilisez l'operator() plutôt que l'operator[].

La méthode la plus propre dans le cas d'indexes multiples consiste à utiliser l'operator() plutôt que l'operator[]. La raison en est que l'operator[] prend toujours un et un seul paramètre, alors que l'operator() peut lui prendre autant de paramètres qu'il est nécessaire (dans le cas d'une matrice rectangulaire, vous avez besoin de deux paramètres).

class Matrix {
   public:
      Matrix(unsigned rows, unsigned cols);
      double& operator() (unsigned row, unsigned col);
      double  operator() (unsigned row, unsigned col) const;
      ...
      ~Matrix();                                 // Destructeur
      Matrix(const Matrix& m);               // Constructeur de copie
      Matrix& operator= (const Matrix& m);   // Opérateur d'assignement
      ...
   private:
      unsigned rows_, cols_;
      double* data_;
};
 
inline Matrix::Matrix(unsigned rows, unsigned cols)
    : rows_ (rows)
    , cols_ (cols)
    //data_ <--initialized below (after the 'if/throw' statement)
{
    if (rows == 0 || cols == 0)
       throw BadIndex("Matrix constructor has 0 size");
    data_ = new double[rows * cols];
}
 
inline Matrix::~Matrix()
{
    delete[] data_;
}
 
inline double& Matrix::operator() (unsigned row, unsigned col)
{
    if (row >= rows_ || col >= cols_)
       throw BadIndex("Matrix subscript out of bounds");
    return data_[cols_*row + col];
}
 
inline double Matrix::operator() (unsigned row, unsigned col) const
{
    if (row >= rows_ || col >= cols_)
       throw BadIndex("const Matrix subscript out of bounds");
    return data_[cols_*row + col];
}
Ainsi, l'accès à un élément de la Matrix m se fait en utilisant m(i,j) plutôt que m[j]:

int main()
{
    Matrix m(10,10);
    m(5,8) = 106.15;
    std::cout << m(5,8);
    //...
}

Pourquoi est-il préférable que l'interface de ma classe Matrix ne soit pas basée sur le modèle du tableau de tableaux ?
auteur : Marshall Cline
De quoi cette question traite-t-elle exactement? Certains programmeurs créent des classes Matrix et leur donnent un operator[] qui renvoie une référence à un objet Array, objet Array qui lui-même possède un operator[] qui renvoie un élément de la matrice (par exemple, une référence sur un double). Ça leur permet d'accéder aux éléments de la matrice en utilisant la syntaxe m[j] plutôt qu'une syntaxe de type m(i,j) .

Cette solution de tableau de tableaux fonctionne, mais elle est moins flexible que la solution basée sur l'operator() . En effet, l'approche utilisant l'operator() offre certaines possibilités d'optimisation qui sont plus difficilement implémentables avec l'approche operator[][]. Cette dernière approche est donc plus susceptible de causer, au moins dans un certain nombre de cas, des problèmes de performances.

Pour vous donner un exemple, la façon la plus simple d'implémenter l'approche operator[][] consiste à représenter physiquement la matrice comme une matrice dense stockant ses éléments en ligne (ou bien est-ce plutôt un stockage en colonne, je ne m'en souviens jamais). L'approche utilisant l'operator() cache elle complètement la représentation physique de la matrice, ce qui peut dans certains cas donner de meilleures performances.

En résumé : l'approche basée sur l'operator() n'est jamais moins bonne et s'avère parfois meilleure que l'approche operator[][].

  • L'approche operator() n'est jamais moins bonne car il est facile de l'implémenter en utilisant la représentation physique "matrice dense - stockage en ligne". Et donc dans les cas où cette représentation physique est la plus adaptée d'un point de vue performance, l'approche operator() est aussi facile à implémenter que l'approche operator[][] (il se pourrait même que l'approche operator() soit légèrement plus facile à implémenter, mais je ne vais pas pinailler).
  • L'approche operator() s'avère parfois meilleure car à partir du moment où la représentation physique optimale n'est pas la représentation "matrice dense - stockage en ligne", il est le plus souvent sensiblement plus facile d'implémenter l'approche operator() que l'approche operator[][].

J'ai travaillé récemment sur un projet qui a illustré l'importance de la différence que peut faire le choix de la représentation physique. L'accès aux éléments de la matrice y était fait colonne par colonne (l'algorithme accédait aux éléments d'une colonne, puis de la suivante, etc.), et dans ce cas, une représentation physique en ligne risquait de diminuer l'efficacité de la mémoire cache. En effet, si les lignes sont presque aussi grosses que la taille du cache du processeur, chaque accès à l'élément suivant dans la colonne va demander à ce que la ligne suivante soit chargée dans le cache, ce qui fait perdre l'avantage que procure un cache. Sur ce projet, nous avons gagné 20% en performance en découplant la représentation logique de la matrice (ligne, colonne) de sa représentation physique (colonne, ligne).

Des exemples de ce type, on en trouve en quantité en calcul numérique et quand on s'attaque au vaste sujet que représentent les matrices creuses. Au final, puisqu'il est en général plus facile d'implémenter une matrice creuse ou d'inverser l'ordre des lignes et des colonnes en utilisant l'operator(), vous n'avez rien à perdre et possiblement quelque chose à gagner à utiliser cette approche.

Utilisez l'approche basée sur l'operator()


Comment surcharger les opérateurs ++ et -- ?
auteur : Marshall Cline
Via un paramètre bidon.

Etant donné que ces opérateurs peuvent avoir deux définitions, le C++ leur donne deux signatures différentes. Les deux s'appellent operator ++(), mais la version pré-incrémentation ne prend pas de paramètre, et l'autre prend un entier bidon. Nous traiterons ici le cas de ++, mais l'opérateur -- se comporte de façon similaire. Tout ce qui s'applique à l'un s'applique donc à l'autre.

class Number {
   public:
      Number& operator++ ();    // prefix ++
      Number  operator++ (int); // postfix ++
};
A remarquer : la différence des types de retour. La version préfixée renvoie par référence, la postfixée par valeur. Si cela semble inattendu, ce sera tout à fait logique après avoir examiné les définitions (vous vous souviendrez ensuite que y = x++ et y = ++x affectent des résultats différents à y).

Number& Number::operator++ ()
{
   // ...
   return *this;
}
 
Number Number::operator++ (int)
{
   Number ans = *this;
   ++(*this);  // ou appeler simplement operator++()
   return ans;
}
L'autre possibilité pour la version postfixée est de ne rien renvoyer :

class Number {
   public:
     Number& operator++ ();
     void    operator++ (int);
};
 
Number& Number::operator++ ()
{
   //...
   return *this;
}
 
void Number::operator++ (int)
{
   ++(*this);  // ou appeler simplement operator++()
}
Attention, il ne faut pas que la version postfixée renvoie l'objet 'this' par référence, vous aurez été prévenus.

Voici comment utiliser ces opérateurs :

 Number x = /* ... */;
 ++x;  // appel de Number::operator++(), c-a-d x.operator++()
 x++;  // appel de Number::operator++(int), c-a-d calls x.operator++(0)
Supposant que les types de retour ne sont pas void, on peut les utiliser dans des expressions plus complexes

 Number x = /* ... */;
 Number y = ++x;  // y sera le nouvelle valeur de x
 Number z = x++;  // z sera la nouvelle valeur de x

Qu'est-ce qui est le plus rapide : i++ ou ++i ?
auteur : Marshall Cline
++i est parfois plus rapide que i++, mais en tout cas n'est jamais plus lent.

Pour les types de base comme les entiers, cela n'a aucune importance : i++ et ++i sont identiques point de vue rapidité. Pour des types manipulant des classes, comme les itérateurs par exemple, ++i peut être plus rapide que i++ étant donné que ce dernier peut prendre une copie de l'objet 'this.'

la différence, pour autant qu'il y en ait une n'aura aucune influence à moins que votre application soit très dépendante de la vitesse du CPU. Par exemple, si votre application attend la plupart du temps que l'utilisateur clique sur la souris, ou qu'elle fasse des accès disques, ou des accès réseau, ou des recherches dans une base de données, cela ne risque pas de poser problème que de perdre quelques cycles CPU.

Si vous écrivez i++ comme une instruction isolée plutôt que comme une partie d'une expression plus complexe, pourquoi ne pas plutôt écrire ++i ? Vous ne perdrez jamais rien, et parfois même vous y gagneriez quelque chose. Les programmeurs habitués à faire du C ont l'habitude d'écrire i++ plutôt que ++i. Par exemple, ils écrivent

for (i = 0; i < 10; i++) ....
Comme cette expression utilise i++ comme une instruction isolée, nous pourrions tout à fait écrire ++i à la place. Pour des raisons de symétrie, j'ai une préférence pour ce style même si cela n'apporte rien au point de vue performance.

De toute évidence, quand i++ apparaît en tant que partie d'une expression plus complexe, la situation est différente : il est utilisé parce que c'est la seule solution logique et correcte et non pas parce qu'il s'agit d'une habitude héritée de l'époque ou l'on codait du C.


Qu'est-ce donc qu'une auto-affectation ?
auteur : Marshall Cline
Une auto-affectation a lieu quand quelqu'un affecte un objet à lui-même.

#include "Fred.hpp"    // Déclaration de la classe Fred

void userCode(Fred& x)
{
      x = x;    // Auto-affectation
}
Bien évidemment, personne n'écrit du code pareil, mais parce que des pointeurs ou des références distinctes peuvent désigner le même objet (c'est l'aliasing), des auto-affectations peuvent avoir lieu derrière votre dos.

void userCode(Fred& x, Fred& y)
{
      x = y;    // C'est une auto-affectation si &x == &y
}

int main()
{
    Fred z;
    userCode(z, z);

return 0;
}

Pourquoi l'auto-affectation peut-elle poser problème ?
auteur : Marshall Cline
Si vous ne prenez pas en compte le cas de l'auto-affectation, vous exposez les utilisateurs de vos classes à des bugs subtils qui peuvent avoir des conséquences désastreuses. Par exemple, l'affectation d'un objet de la classe ci-dessous à lui-même va poser un très gros problème.

class Wilma 
{ 
};

class Fred {
    public:
      Fred()                : p_(new Wilma())      { }
      Fred(const Fred& f)   : p_(new Wilma(*f.p_)) { }
     ~Fred()                { delete p_; }
      Fred& operator= (const Fred& f)
        {
          // Ce code n'est pas bon : il ne traite pas le cas de l'auto-affectation !
          delete p_;             // Ligne 1
          p_ = new Wilma(*f.p_);     // Ligne 2
          return *this;
        }
    private:
      Wilma* p_;
};
Si quelqu'un assigne un objet de type Fred à lui-même, la ligne 1 va détruire à la fois this->p_ et f.p_ puisque *this et f désignent ici le même objet. Juste derrière, la ligne 2 utilise *f.p_, mais cet objet n'est plus valide puisqu'il a été détruit. Inutile de vous dire que cette utilisation risque fort de s'avérer catastrophique.

Retenez qu'il est votre responsabilité, en tant qu'auteur de la classe Fred, de garantir que l'affectation d'un objet de type Fred à lui-même ne pose pas de problèmes. Ne partez pas du principe que les utilisateurs de vos classes ne feront jamais ce genre d'affectation. Et ce sera de votre faute si un objet de votre classe fait crasher le programme dans le cas où on l'affecte à lui-même.

Notez aussi que dans l'exemple ci-dessus, l'opérateur Fred::operator= (const Fred&) contient un autre bug : Si une exception est lancée lors de l'évaluation de new Wilma(*f.p_) (par exemple, une exception plus-de-mémoire ou une exception lancée par le constructeur par copie de Wilma), this->p_ va se retrouver pointant sur de la mémoire qui n'est plus valide. La solution consiste à allouer les nouveaux objets avant de détruire les anciens.


Comment éviter les problèmes d'auto-affectation ?
auteur : Marshall Cline
Vous devez vous poser la question de l'auto-affectation à chaque fois que vous créez une classe. Mais ça ne veut pas dire qu'il est nécessaire que vous ajoutiez du code à toutes vos classes : ajouter du code n'est pas utile si vos objets peuvent être assignés à eux-mêmes sans que cela pose un problème.

Si vous devez modifier votre opérateur d'affectation, voici une technique simple et efficace :

Fred& Fred::operator= (const Fred& f)
{
    if (this == &f) return *this;  // Traite correctement le cas de  l'auto-affectation

    // Ici se trouve le code normal de l'opérateur d'affectation...

    return *this;
}
Ce test explicite n'est pas toujours nécessaire. Par exemple, si vous vouliez corriger l'opérateur d'affectation de la question précédente de façon que les exceptions lancées par new et/ou les exceptions lancées par le constructeur de copie de la classe Wilma soient gérées correctement, vous écririez le code suivant. Et notez que ce code a pour (agréable) effet de bord de traiter correctement le cas de l'auto-affectation :

Fred& Fred::operator= (const Fred& f)
{
    // Ce code traite correctement (même si c'est implicite) le cas de l'auto-affectation 
   Wilma* tmp = new Wilma(*f.p_); // Pas de problème si une exception était levée ici
   delete p_;
   p_ = tmp;
   return *this;
}
Dans un cas comme ci-dessus (où l'auto-affectation est sans danger mais inefficace), certains programmeurs veulent quand même ajouter "if (this == &f) return *this;" pour obtenir de meilleures performances dans le cas d'une auto-affectation. C'est la plupart du temps un mauvais choix, car si une auto-affectation a lieu une fois sur mille, alors le if consommera inutilement des cycles processeur dans 99,9% des cas.


precedent    sommaire    suivant   

Consultez les autres F.A.Q's

Valid XHTML 1.1!Valid CSS!


Ce document issu de http://www.developpez.com est soumis à la licence GNU FDL traduit en français ici.
Permission vous est donnée de distribuer, modifier des copies de cette page tant que cette note apparaît clairement.
Certaines parties de ce document sont sous copyright Marshall Cline Les codes sources présentés sur cette page sont libres de droits, et vous pouvez les utiliser à votre convenance. Pour le reste, ce document constitue une oeuvre intellectuelle protégée par les droits d'auteurs. Ce document issu de http://www.developpez.com est soumis à trois licences, en fonction des contributeurs :
- Les contributions de Clément Cunin et LFE sont soumises aux termes de la la licence GNU FDL traduite en français ici. Permission vous est donnée de distribuer, modifier des copies des contributions de Clément Cunin et LFE tant que cette note apparaît clairement :
"Ce document issu de http://www.developpez.com est soumis à la licence GNU FDL traduite en français ici. Permission vous est donnée de distribuer, modifier des copies de cette page tant que cette note apparaît clairement".
- Les contributions de Marshall Cline sont sous copyright
- Pour ce qui est des autres contributions : Copyright © 2005 Developpez LLC : Tous droits réservés Developpez LLC. Aucune reproduction, ne peut en être faite sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts. Cette page est déposée à la SACD.
Vos questions techniques : forum d'entraide Accueil - Publiez vos articles, tutoriels, cours et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones. Nous contacter - Copyright 2000..2005 www.developpez.com