Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

Vous n'avez pas encore de compte Developpez.com ? L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Developpez.com

C++

Choisissez la catégorie, puis la rubrique :

logo
Sommaire > Les opérateurs > La surcharge d'opérateurs
        Qu'est-ce que la surcharge d'opérateur ?
        Quel est l'avantage de surcharger un opérateur ?
        Quelques exemples de surcharge d'opérateur
        La surcharge d'opérateur n'embellit pas vraiment ma classe ; ce n'est pas censé rendre le code plus lisible ?
        Quels opérateurs peut-on ou ne peut-on pas surcharger ?
        Peut-on surcharger operator== de façon qu'il compare deux char[] en faisant une comparaison de chaîne ?
        Peut-on définir un operator** qui calcule 'x à la puissance y' ?
        Comment implémenter un opérateur d'indexation pour une classe Matrix ?
        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 ?
        Comment surcharger les opérateurs ++ et -- ?
        Qu'est-ce qui est le plus rapide : i++ ou ++i ?
        Qu'est-ce que l'auto-affectation ?
        Comment écrire un opérateur d'affectation correct ?
        J'ai pour surcharger mon opérateur la possibilité d'utiliser une fonction membre ou une fonction libre, que choisir ?
        Comment surcharger correctement l'opérateur == ?
        Comment surcharger correctement l'opérateur < ?



Qu'est-ce que la surcharge d'opérateur ?
Créé le 19/03/2004[haut]
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

    // Sans surcharge d'opérateur
    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));    // Hum...
    }
 
#else
 
    // Avec surcharge d'opérateur
    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 ?
Créé le 19/03/2004[haut]
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
Créé le 19/03/2004[haut]
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 ?
Mise à jour le 17/03/2008[haut]
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 vous 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 ?
Créé le 19/03/2004[haut]
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 ?
Créé le 19/03/2004[haut]
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 classe 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" ?
Mise à jour le 30/08/2004[haut]
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 ?
Créé le 19/03/2004[haut]
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_ est initialisé en suivant (après l'instruction 'if/throw')
{
    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 ?
Créé le 19/03/2004[haut]
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 réalisables 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 -- ?
Créé le 19/03/2004[haut]
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 aura le nouvelle valeur de x
 Number z = x++;  // z aura la nouvelle valeur de x

Qu'est-ce qui est le plus rapide : i++ ou ++i ?
Créé le 19/03/2004[haut]
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 que l'auto-affectation ?
Créé le 03/02/2007[haut]
auteurs : Laurent Gomila, 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 parle-t-on de l'auto-affectation ? Parce qu'elle peut être dangereuse. Imaginez une classe gérant un pointeur brut, et son opérateur d'affectation :

class MaClasse
{
private :

    Ressource* ptr;

public :

    MaClasse& operator =(const MaClasse& Other)
    {
        // Destruction de ptr
        delete this->ptr;

        // Réallocation et affectation de ptr
        this->ptr = new int(*Other.ptr);

        return *this;
    }
};
Dans le cas d'une auto-affectation, this et Other pointent vers la même instance, et donc vers le même ptr. Je vous laisse imaginer ce qu'il se passe lorsqu'on essaye de lire Other.ptr alors qu'il vient d'être détruit à la ligne précédente.

Pour éviter les problèmes d'auto-affectation, ou simplement pour tenter d'optimiser le code, on ajoute souvent un simple test permettant de vérifier que les deux instances sont différentes ; dans le cas contraire on peut quitter sans effectuer d'affectation.

MaClasse& MaClasse::operator =(const MaClasse& Other)
{
    if (this != &Other)
    {
        // Destruction de ptr
        delete this->ptr;

        // Réallocation et affectation de ptr
        this->ptr = new int(*Other.ptr);
    }

    return *this;
}
Mais attention ce code aussi est un piège : en effet nous ajoutons un test que l'on croit utile, mais qui sera dans 99.9% des cas effectué pour rien (n'oubliez pas que l'auto-affectation est tout de même très rare). D'autant plus que si vous écrivez correctement votre opérateur d'affectation, comme indiqué dans la question Comment écrire un opérateur d'affectation correct ?, les éventuels problèmes d'auto-affectation sont résolus automatiquement de manière élégante.


Comment écrire un opérateur d'affectation correct ?
Créé le 03/02/2007[haut]
auteur : Laurent Gomila
La première chose à connaître est le prototype correct d'un opérateur d'affectation :

MaClasse& MaClasse::operator =(const MaClasse& Other)
{
    // ...
    return *this;
}
Le retour de this permettra de chaîner les affectations (a = b = c).
Le paramètre Other est pris par référence constante car celui-ci ne doit pas (sauf cas très spécifiques) être modifié par la fonction ; cela permettra de recevoir ce que l'on appelle des temporaires non nommés (retours de fonction, objets construits à la volée, ...). Une autre erreur potentielle, parfois commise par les débutants, est de mal comprendre le fonctionnement de l'opérateur = et d'affecter this à Other ; la référence constante provoquera des erreurs de compilation et permettra de le détecter immédiatement.
Quant au fait que l'on renvoie une référence non constante, il y a deux raisons à cela. La première étant que cela assure que nous ne serons pas tentés de renvoyer le paramètre (Other ici), ce qui serait incorrect ; la seconde raison est que le retour d'une affectation peut être modifié (donc non constant) pour les types primitifs, par souci de cohérence il est donc bon de faire de même pour les types que vous écrivez.

La seconde chose à savoir est que le compilateur génère un opérateur d'affectation automatiquement si vous ne le faites pas, et que celui-ci sera suffisant dans la plupart des cas. Vous pouvez donc parfois tout simplement éviter de l'écrire.

Il faudra écrire explicitement un opérateur d'affectation dès lors qu'une simple affectation membre à membre des données de votre classe n'est plus suffisante, par exemple si elle gère une ressource (un pointeur brut, une connexion à une base de données, une texture en mémoire vidéo, ...).

Il existe également des situations où l'on ne veut pas de l'opérateur d'affectation généré par le compilateur, notamment pour les classes dont les instances ne doivent pas être copiées (une base de données, un singleton, etc.). Dans ce cas, une bonne pratique est d'en interdire l'utilisation en le déclarant privé et en ne le définissant pas (ie. ne pas écrire son corps). Il en va d'ailleurs de même pour le constructeur par copie, les deux allant généralement de paire.

Pour définir l'opérateur d'affectation d'une classe gérant une ressource brute, on serait tenté d'écrire ce genre de code :

MaClasse& MaClasse::operator =(const MaClasse& Other)
{
    // Destruction de la ressource (ici un pointeur brut)
    delete this->Ressource;

    // Allocation d'une nouvelle
    this->Ressource = new ClasseRessouce;

    // Copie de la ressource
    *this->Ressource = *Other.Ressource;

    return *this;
}
Mais ce code est incorrect. En effet, que se passe-t-il si l'on se trouve dans le cas (rare) d'une auto-affectation ? this et Other seront la même instance, et pointeront donc sur la même ressource. Au moment où l'on va tenter de la copier, elle aura été détruite, provoquant un comportement indéterminé.

De même, imaginez que l'appel à new échoue (ce qui peut très bien arriver) : la ressource aura déjà été détruite, mais ne sera pas recréée, laissant l'objet dans un état invalide, et menant là encore à des comportements indéterminés.

La solution à cela est d'effectuer toutes les allocations (plus généralement, les choses qui sont susceptibles de lever des exceptions) en premier, puis si tout a réussi, alors on peut effectuer les libérations.

Voici une version plus correcte de l'opérateur d'affectation précédent :

MaClasse& MaClasse::operator =(const MaClasse& Other)
{
    // Réallocation de la ressource
    ClasseRessource* NouvelleRessource = new ClasseRessource(Other.Ressource);

    // Destruction de la ressource
    delete this->Ressource;

    // Réaffectation de la nouvelle
    this->Ressource = NouvelleRessource;

    return *this;
}
Cette version est correcte sur tous les plans : en cas d'auto-affectation la ressource sera bien recopiée avant d'être détruite, et en cas d'exception pendant l'allocation, l'objet sera toujours tel qu'il était avant l'appel à l'opérateur d'affectation.

Nous pouvons aller encore un peu plus loin, en constatant que finalement tout ceci est déjà plus ou moins implémenté dans votre classe. En effet, si celle-ci est bien codée, la réallocation de la ressource est exactement le boulot du constructeur par copie, et la destruction celui du destructeur.

Ainsi une version plus élégante du code précédent serait la suivante :

MaClasse& MaClasse::operator =(const MaClasse& Other)
{
    // Utilisation du constructeur par copie pour copier la ressource MaClasse
    MaClasse Temp(Other);

    // Réaffectation : on prend les nouvelles données dans Temp, et on lui
    // donne les données à détruire en échange
    std::swap(Temp.Ressource, this->Ressource);

    return *this;
} // A la fin de la fonction, Temp sera détruit automatiquement
  // et son destructeur désallouera correctement la ressource
Avec donc un constructeur par copie et un destructeur correctement implémentés :

// Le constructeur par copie
MaClasse::MaClasse(const MaClasse& Other)
{
    if (Other.Ressource)
        this->Ressource = new ClasseRessource(*Other.Ressource);
    else
        this->Ressource = NULL;
}

// Le destructeur
MaClasse::~MaClasse()
{
    delete this->Ressource;
}

J'ai pour surcharger mon opérateur la possibilité d'utiliser une fonction membre ou une fonction libre, que choisir ?
Créé le 15/10/2009[haut]
auteur : JolyLoic
Par exemple, pour définir un opérateur + dans une classe A, on peut écrire :
Version avec une fonction membre

class A
{
A operator+(A const & second);
};
Version avec une fonction libre

class A
{
};

A operator+(A const &first, A const &second);
Quelle est la version préférable ? En général, pour un opérateur binaire, il s'agit de la fonction libre, car elle respecte la symétrie que l'on s'attend à trouver entre les opérandes d'un tel opérateur, alors que la fonction membre considère que l'élément sur lequel elle agit (le this) doit être exactement du type voulu.

En particulier, imaginons que l'on puisse convertir un entier en une variable de type A (par exemple si A possède un constructeur non explicite prenant uniquement un entier en paramètre). Alors on a le comportement suivant :
Version avec une fonction membre

A a1, a2;
a1 + a2; // Ok
a1 + 42; // Ok
42 + a2; // Ne compile pas
Version avec une fonction libre

A a1, a2;
a1 + a2; // Ok
a1 + 42; // Ok
42 + a2; // Ok
Il y a par contre des opérateurs que l'on n'a le droit de surcharger que comme fonction membre : operator=, operator(), operator[] et operator->.


Comment surcharger correctement l'opérateur == ?
Créé le 15/10/2009[haut]
auteur : JolyLoic
Bien qu'il soit possible de faire ce que l'on veut quand on surcharge un opérateur, il y a des règles à respecter si on a envie que notre surcharge marche bien avec le reste du langage, et sans mauvaise surprise pour l'utilisateur. Ainsi, un opérateur == qui modifierait ses paramètres serait très malvenu.

En plus de ces règles de bon sens, un opérateur == bien éduqué doit répondre à des critères supplémentaires, ce que les mathématiciens indiquent en disant qu'il doit définir une relation d'équivalence. Voici ces critères :

  • Pour tout a, a==a (un objet doit être identique à lui même).
  • Pour tout a et b, si a==b, alors b==a (être égal marche dans les deux sens).
  • Pour tout a, b et c, si a==b et b==c, alors a==c (on dit que c'est transitif).
Généralement, on écrit naturellement des opérateur == qui respectent ces règles, sans même le savoir, mais il y a quand même des possibilités d'erreur. Un exemple de code qui ne marche pas :

class Double
{
public:
  Double(double d) : myValue(d) {}
  double val() {return myValue;}
private:
  double myValue;
};

bool operator== (Double const &d1, Double const &d2)
{
  static double const epsilon = 0.001;
  return d2.val() - d1.val() < epsilon;
}
Le problème avec ce code, pourtant bien intentionné, qui veut définir que deux Doubles sont à considérer comme identiques s'ils sont suffisamment proches l'un de l'autre est qu'il ne respecte pas la condition de transitivité. Ainsi :


Double a(0), b(0.0009), c(0.0011);
assert(a==b); // Ok
assert(b==c); // Ok
assert(a==c); // Erreur
Remarque : Ces règles s'appliquent aussi à un prédicat de type égalité que l'on passerait à un algorithme.


Comment surcharger correctement l'opérateur < ?
Créé le 15/10/2009[haut]
auteur : JolyLoic
De même que l'opérateur ==, l'opérateur < se doit de respecter certaines règles. Ces règles sont moins évidentes que pour l'opérateur ==, aussi est-il facile de se tromper. Les voici :

  • Pour tout a, a<a est faux.
  • Pour tout a, b et c, si a<b et b<c alors a<c.
  • Pour tout a et b, si a<b est faux et b<a est faux, on dit que a et b sont équivalents (cette relation doit être une relation d'équivalence, comme précisée dans la question sur l'opérateur ==.
Un cas classique où l'on a besoin de définir l'opérateur < est quand un objet se compose de sous-objets, et que la relation d'ordre sur les objets dépend de celle des sous-objets. On voit souvent du code comme :

class Personne
{
public:
  string nom;
  string prenom;
};

bool operator< (Personne const &p1, Personne const &p2)
{
  return p1.nom < p2.nom && p1.prenom < p2.prenom;
}
Si l'on prend par exemple les personnes suivantes : a = {"Stroustrup", "Bjarne"} et b = {"Clamage", "Steve"} on a :

  • a<b qui est faux (car "Stroustrup" < "Clamage" est faux) ;
  • b<a qui est faux (car "Steve" < "Bjarne" est faux).
Ce qui implique que ces deux personnes sont équivalentes (si on insérait les deux dans une map, par exemple, la map ne contiendrait qu'un seul élément).

J'ai souvent vu cette tentative de correction :

bool operator< (Personne const &p1, Personne const &p2)
{
  if (p1.nom < p2.nom)
    return true;
  else
    return p1.prenom < p2.prenom;
}
Mais ce code ne marche pas non plus (cette fois ci, on a a<b et b<a qui sont tous deux simultanément vrais). Une bonne solution ressemble plutôt à :

bool operator< (Personne const &p1, Personne const &p2)
{
  if (p1.nom < p2.nom)
    return true;
  else if (p2.nom < p1.nom)
    return false;
  else
    return p1.prenom < p2.prenom;
}
Remarque : Ces règles s'appliquent aussi à un prédicat de type inférieur que l'on passerait à un algorithme ou en argument d'un conteneur. On pourrait imaginer des règles semblables pour la surcharge des opérateurs <, <=,... mais en pratique, comme, sauf surprise, ces opérateurs sont reliés entre eux, c'est devenu une habitude en C++ de se limiter à l'utilisation de < dans les algorithmes et conteneurs.



Consultez les autres F.A.Q.


Valid XHTML 1.0 TransitionalValid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C++

Partenaire : Hébergement Web