IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo

FAQ C++Consultez toutes les FAQ

Nombre d'auteurs : 34, nombre de questions : 368, dernière mise à jour : 14 novembre 2021  Ajouter une question

 

Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.

Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.

Sur ce, nous vous souhaitons une bonne lecture.

SommaireLes classes en C++La surcharge d'opérateurs (18)
précédent sommaire suivant
 

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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

Mis à jour le 19 mars 2004 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 bogues.

Mis à jour le 19 mars 2004 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.

Mis à jour le 19 mars 2004 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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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]; 
}

Mis à jour le 19 mars 2004 Cline

Par exemple, pour définir un opérateur + dans une classe A, on peut écrire :

Code c++ : Sélectionner tout
1
2
3
4
5
class A 
{ 
public: 
    A operator+(A const & second); 
};
Code c++ : Sélectionner tout
1
2
3
4
5
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 :

Code c++ : Sélectionner tout
1
2
3
4
A a1, a2; 
a1 + a2; // Ok 
a1 + 42; // Ok 
42 + a2; // Ne compile pas
Code c++ : Sélectionner tout
1
2
3
4
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->.

Mis à jour le 15 octobre 2009 JolyLoic

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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
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.

Mis à jour le 17 mars 2008 Cline

L'opérateur d'affectation permet de copier la valeur d'un objet dans un autre :

Code c++ : Sélectionner tout
1
2
3
4
5
class CMyClass 
{ 
public: 
    CMyClass& operator=(CMyClass const&); // opérateur d'affectation 
};
Si la classe ne définit pas d'opérateur d'affectation, le compilateur en génère un implicitement. Celui-ci appelle l'opérateur d'affectation des membres.
À noter que si la classe suit une sémantique d'entité, la définition d'un opérateur d'affectation a de grandes chances d'être une erreur de conception. Il faut alors le déclarer privé sans le définir de façon à interdire son utilisation.

Mis à jour le 15 octobre 2009 3DArchi

Tant que votre classe ne doit pas gérer de ressources brutes (pointeurs pour lesquels elle serait responsable de l'allocation dynamique de la mémoire), il est souvent préférable de laisser simplement le compilateur créer cet opérateur lui-même.

En effet, l'opérateur d'affectation est l'une des fonctions qui seront automatiquement générées par le compilateur s'il n'en trouve aucune implémentation de votre cru, ce qui fait que, dans bien des cas, vous risquez simplement de perdre énormément de temps à (mal) faire ce que le compilateur aurait très bien été capable de faire correctement.

Cependant, chaque fois que votre classe doit gérer elle-même de la mémoire allouée et libérée dynamiquement (avec respectivement new ou new[] dans ses constructeurs et delete ou delete[] dans son destructeur), vous ne pourrez plus faire confiance à l'opérateur d'affectation automatiquement généré par le compilateur, et vous devrez donc en créer un en y apportant le plus grand soin.

Vous devrez en effet veiller :

  • au fait que la mémoire allouée au contenu d'origine de votre variable soit correctement détruit si l'affectation réussit ;
  • au fait qu'il importe d'éviter que les pointeurs de deux objets pointent sur une même adresse mémoire à libérer de manière dynamique afin d'éviter les risques de double libération de la mémoire ;
  • au fait que toute tentative d'allocation dynamique de la mémoire est susceptible d'échouer par défaut d'une quantité de mémoire contiguë suffisante.

Afin de prendre toutes ces restrictions en compte, il s'agira d'utiliser l'idiome copy and swap que l'on pourrait traduire par « copie et permutation des données ».

Le principe de cet idiome est, après s'être occupé du comportement correct du constructeur par copie et du destructeur, d'effectuer une copie sur la pile (AKA : sans avoir recours à l'allocation dynamique de la mémoire) de l'objet à affecter, puis d'intervertir un à un les membres de la copie et de l'objet en cours.

De cette manière, lorsque nous quitterons la portée de l'opérateur d'affectation, la copie sera automatiquement détruite et les ressources qui appartenaient à l'origine à notre objet de destination seront correctement libérées.

Nous avons l'habitude d'entendre que, bien souvent, il est préférable de passer les objets plus important que des types primitifs sous la forme de référence, éventuellement constante (si la fonction appelée ne doit pas les modifier) afin d'en éviter une copie qui peut être lente et gourmande en ressources.

Cependant, de nombreux compilateurs actuels permettent d'éviter plusieurs copies inutiles grâce à des processus nommés (N)RVO.

Et, comme nous devons de toutes manières travailler sur une copie de l'objet « source » à affecter, nous pouvons nous donner une chance supplémentaire de profiter de ce type d'optimisation en passant l'objet source non plus par référence, mais en le passant directement par valeur.

L'opérateur d'affectation prendra donc une forme proche de
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
/* le constructeur par copie est automatiquement appelé */ 
MaClasse& MaClasse::operator =(MaClass Other) 
{ 
    /* à faire pour chaque membre */ 
    std::swap(membre,Other.membre); // permutons le membre de l'objet en cours et de la copie 
    /* renvoyons l'objet en cours */ 
    return *this; 
}// Other (qui contient maintenant les membres d'origine de l'objet courant) 
 // est automatiquement détruit et ses membres correctement détruits
Le retour de this permettra de chaîner les affectations (a = b = c).

Le fait de passer le paramètre par valeur provoquera directement, c'est ce que l'on désire, l'appel du constructeur par copie de l'objet.

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, et par souci de cohérence, il est donc bon de faire de même pour les types que vous écrivez.

Mis à jour le 3 février 2007 koala01 Laurent Gomila

Une auto-affectation a lieu quand quelqu'un affecte un objet à lui-même.

Code c++ : Sélectionner tout
1
2
3
4
5
6
#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.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
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.

Mis à jour le 3 février 2007 Laurent Gomila

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.

Mis à jour le 19 mars 2004 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é.

Mis à jour le 30 août 2004 Cline

Utilisez l'operator() plutôt que l'operator[].

La méthode la plus propre dans le cas d'indices 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).

Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <vector> 
  
class Matrix { 
public: 
    Matrix(unsigned rows, unsigned cols); 
    Matrix(const Matrix&) = default;                // Constructeur par copie 
    Matrix& operator= (const Matrix&) = default;    // Opérateur d'affectation par copie 
    Matrix(Matrix &&) = default;                    // Constructeur par déplacement 
    Matrix& operator= (Matrix &&) = default;        // Opérateur d'affectation par déplacement 
    ... 
    double& operator() (unsigned row, unsigned col); 
    double  operator() (unsigned row, unsigned col) const; 
private: 
    unsigned rows_, cols_; 
    std::vector<double> data_; 
};
Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <cassert> 
  
Matrix::Matrix(unsigned rows, unsigned cols) 
    : rows_ (rows) 
    , cols_ (cols) 
    , data_(rows * cols) 
{ } 
  
  
double& Matrix::operator() (unsigned row, unsigned col) 
{ 
    assert(row < rows_ && col < cols_ && "Matrix subscript out of bounds"); 
    return data_[cols_*row + col]; 
} 
  
double Matrix::operator() (unsigned row, unsigned col) const 
{ 
    assert(row < rows_ && col < cols_ && "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[i][j] :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
int main() 
{ 
    Matrix m(10,10); 
    m(5,8) = 106.15; 
    std::cout << m(5,8); 
    //... 
}

Mis à jour le 22 mai 2018 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()

Mis à jour le 19 mars 2004 Cline

Via un paramètre bidon.

Étant 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.

Code c++ : Sélectionner tout
1
2
3
4
5
class Number { 
public: 
    Number& operator++ ();    // prefix ++ 
    Number  operator++ (int); // postfix ++ 
};
À 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).

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 :

Code c++ : Sélectionner tout
1
2
3
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

Code c++ : Sélectionner tout
1
2
3
Number x = /* ... */; 
Number y = ++x;  // y aura le nouvelle valeur de x 
Number z = x++;  // z aura la nouvelle valeur de x

Mis à jour le 19 mars 2004 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

Code c++ : Sélectionner tout
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.

Mis à jour le 19 mars 2004 Cline

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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 :

Code c++ : Sélectionner tout
1
2
3
4
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.

Mis à jour le 15 octobre 2009 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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
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 :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
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 à :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
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.

Mis à jour le 15 octobre 2009 JolyLoic

Habituellement, pour pouvoir envoyer un objet sur un flux on surcharge l'opérateur << entre un ostream et le type de notre objet. Cependant, cette fonction ne peut être membre et de ce fait elle ne peut pas non plus être virtuelle. Par contre rien ne l'empêche d'appeler une fonction membre virtuelle de l'objet passé en paramètre. Ainsi, la manière habituelle de surcharger l'opérateur << pour une hiérarchie de classes est la suivante :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>  
  
class Base  
{  
    friend std::ostream& operator << (std::ostream& O, const Base& B);  
  
    virtual void Print(std::ostream& O) const  
    {  
        O << "Je suis une base";  
    }  
};  
  
class Derivee : public Base  
{  
    virtual void Print(std::ostream& O) const  
    {  
        O << "Je suis une derivee";  
    }  
};  
  
std::ostream& operator << (std::ostream& O, const Base& B)  
{  
    B.Print(O);  
    return O;  
}  
  
Base* B1 = new Base,  
Base* B2 = new Derivee;  
  
std::cout << *B1 << std::endl; // "Je suis une base"  
std::cout << *B2 << std::endl; // "Je suis une derivee"

Mis à jour le 18 avril 2005 Laurent Gomila

Proposer une nouvelle réponse sur la FAQ

Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour ça


Réponse à la question

Liens sous la question
précédent sommaire suivant
 

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 © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.