[13] La surcharge d'opérateur

(Une partie de C++ FAQ Lite fr, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)

Traduit de l'anglais par Fabrice Clerc

Les FAQs de la section [13]


[13.1] à quoi sert la surcharge d'opérateur?
à faire en sorte que vos classses soient plus faciles à utiliser.

La surcharge d'opérateur permet de donner aux opérateurs C/C++ un comportement spécifique quand ils sont appliqués à des types spécifiques (des classes). Les opérateurs surchargés remplacent avantageusement les appels de fonction (c'est ce que l'on appelle du "sucre syntaxique"):

class Fred {
public:
// ...
};

#if 0

// Si on n'utilise pas la surcharge:
Fred add(Fred, Fred);
Fred mul(Fred, Fred);

Fred f(Fred a, Fred b, Fred c)
{
return add(add(mul(a,b), mul(b,c)), mul(c,a)); // Beurk...
}

#else

// Avec la surcharge:
Fred operator+ (Fred, Fred);
Fred operator* (Fred, Fred);

Fred f(Fred a, Fred b, Fred c)
{
return a*b + b*c + c*a;
}

#endif

[ Haut | Bas | Rechercher ]


[13.2] Quels avantages y-a-t-il à surcharger un opérateur?
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.

[ Haut | Bas | Rechercher ]


[13.3] Des exemples de surcharge d'opérateur?
Parmi les nombreux exemples que l'on pourrait citer:

[ Haut | Bas | Rechercher ]


[13.4] La surcharge d'opérateur n'embellit pas vraiment ma classe ; ce n'est pas censé rendre le code plus lisible?

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 developpeur 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.

[ Haut | Bas | Rechercher ]


[13.5] Quels opérateurs peut-on ou ne peut-on pas surcharger?

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];
}

[ Haut | Bas | Rechercher ]


[13.6] Peut-on surcharger operator== de façon à ce qu'il compare deux char[] en faisant une comparaison de chaîne?

Non, car aux moins l'une 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 une classe string plutôt qu'un tableau de char, car les tableaux vous veulent du mal.

[ Haut | Bas | Rechercher ]


[13.7] Peut-on écrire un operator** qui calcule "x à la puissance y"?
Ben 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 surchager la fonction pow(base,exponent) (<math.h> 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é.

[ Haut | Bas | Rechercher ]


[13.8] Comment implémenter un opérateur d'indexation pour une classe Matrix?

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èters qu'il est nécessaire (dans le cas d'une matrice rectangulaire, vous avez besoin de deux paramètres).

ça donne par exemple:

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'affectation
// ...
private:
unsigned rows_, cols_;
double* data_;
};

inline
Matrix::Matrix(unsigned rows, unsigned cols)
: rows_ (rows),
cols_ (cols),
data_ (new double[rows * cols])
{
if (rows == 0 || cols == 0)
throw BadIndex("Matrix constructor has 0 size");
}

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 Matrixm se fait en utilisant m(i,j) plutôt que m[i][j]:

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

[ Haut | Bas | Rechercher ]


[13.9] 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?
[FAQ créée récemment (10/99). Les FAQs modifiées récemment sont "chaînées". Cliquez ici pour aller à la suivante.]

De quoi cette FAQ 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[i][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 possiblité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 élements 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[][].

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 performances 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().

[ Haut | Bas | Rechercher ]


[13.10] La conception d'une classe doit-elle se faire plutôt par l'extérieur (on s'intéresse d'abord à l'interface) ou par l'intérieur (on s'intéresse d'abord aux données)?

Par l'extérieur!

Une bonne interface fournit une vue simplifiée exprimée dans le vocabulaire de l'utilisateur. Dans le cas de la programmation par objets, une interface est généralement représentée par une classe unique ou par un groupe de classes très proches .

Réfléchissez d'abord à ce qu'un objet de la classe est du point de vue logique, plutôt que de réfléchir à la façon dont vous allez le représenter physiquement. Imaginez par exemple que vous ayiez une classe Stack (une pile) et que vous vouliez que son implémentation utilise une LinkedList (une liste chaînée):

class Stack {
public:
// ...
private:
LinkedList list_;
};

La classe Stack doit-elle avoir une méthode get() qui retourne la LinkedList? Ou une méthode set() qui prenne une LinkedList? Ou encore une constructeur qui prenne une LinkedList? La réponse est évidemment non, puisque la conception d'une classe doit s'effectuer de l'extérieur vers l'intérieur. Les utilisateurs des objets Stack n'ont rien à faire des LinkedLists; ce qui les intéresse, c'est de pouvoir faire des push (empiler) et des pop (dépiler).

Voyons maintenant un cas un peu plus subtil. Supposez que l'implémentation de a classe LinkedList soit basée sur une liste chaînée d'objets Node (noeuds), et que chaque Node ait un pointeur sur le Node suivant:

class Node { /*...*/ };

class LinkedList {
public:
// ...
private:
Node* first_;
};

La classe LinkedList doit-elle avoir une méthode get() qui donne accès au premier Node? L'objet Node doit-il avoir une méthode get() qui permette aux utilisateurs de passer au Node suivant dans la chaîne? La question est en fait: à quoi une LinkedList doit-elle ressembler vu de l'extérieur? Une LinkedList est-elle vraiment une chaîne d'objets Node? Ou cela n'est-il finalement qu'un détail d'implementation? Et si c'est juste un détail d'implémentation, comment la LinkedList va-t-elle donner à ses utilisateurs la possibilité d'accéder à chacun de ses éléments?

Une réponse parmi d'autres: une LinkedList n'est pas une chaîne d'objets Nodes. C'est peut-être bien comme ça qu'elle est implémentée, mais ce n'est pas ce qu'elle est. Ce qu'elle est, c'est une suite d'éléments. L'abstraction LinkedList doit donc être fournie avec une classe "LinkedListIterator", et c'est cette classe "LinkedListIterator" qui doit disposer d'un operator++ permettant de passer à l'élément suivant, ainsi que de méthodes get()/set() donnant accès à la valeur stockée dans un Node (la valeur stockée dans un Node est sous l'unique responsabilité de l'utilisateur de la LinkedList, c'est pourquoi il faut des méthodes get()/set() permettant à cet utilisateur de la manipuler comme il l'entend).

Toujours du point de vue de l'utilisateur, il pourrait être souhaitable que la classe LinkedList offre un moyen d'accéder à ses éléments qui mimique la façon dont on accède aux éléments d'un tableau en utilisant l'arithmétique des pointeurs:

void userCode(LinkedList& a)
{
for (LinkedListIterator p = a.begin(); p != a.end(); ++p)
cout << *p << '\n';
}

Pour implémenter cette interface, la LinkedList va avoir besoin d'une méthode begin() et d'une méthode end(). Ces méthodes devront renvoyer un objet de type "LinkedListIterator". Et cet objet "LinkedListIterator" aura lui besoin: d'une méthode pour se déplacer vers l'avant (de façon à pouvoir écrire ++p); d'une méthode pour pouvoir accéder à la valeur de l'élément courant (de façon à pouvoir écrire *p); et d'un opérateur de comparaison (de façon à pouvoir écrire p != a.end()).

Le code se trouve ci-dessous. L'idée centrale est que la classe LinkedList n'a pas de méthode donnant accès aux Nodes. Les Nodes sont une technique d'implémentation, technique qui est complètement masquée. Les internes de la classe LinkedList pourraient tout à fait être remplacés par une liste doublement chaînée, ou même par un tableau, avec pour seule différence une modification au niveau de la performance des méthodes prepend(elem) et append(elem).

#include <assert.h> // Succédané de gestion d'exceptions

typedef int bool; // On aura bientôt plus besoin de ça

class LinkedListIterator;
class LinkedList;

class Node {
// Pas de membres public; c'est une "classe privée"
friend LinkedListIterator; // Une classe amie
friend LinkedList;
Node* next_;
int elem_;
};

class LinkedListIterator {
public:
bool operator== (LinkedListIterator i) const;
bool operator!= (LinkedListIterator i) const;
void operator++ (); // Aller à l'élément suivant
int& operator* (); // Accèder à l'élément courant
private:
LinkedListIterator(Node* p);
Node* p_;
};

class LinkedList {
public:
void append(int elem); // Ajoute elem après le dernier élément
void prepend(int elem); // Ajoute elem avant le premier élément
// ...
LinkedListIterator begin();
LinkedListIterator end();
// ...
private:
Node* first_;
};

Les méthodes suivantes sont de bonnes candidates pour être inline (à mettre sans doute dans le même .h):

inline bool LinkedListIterator::operator== (LinkedListIterator i) const
{
return p_ == i.p_;
}

inline bool LinkedListIterator::operator!= (LinkedListIterator i) const
{
return p_ != i.p_;
}

inline void LinkedListIterator::operator++()
{
assert(p_ != NULL); // ou bien if (p_==NULL) throw ...
p_ = p_->next_;
}

inline int& LinkedListIterator::operator*()
{
assert(p_ != NULL); // ou bien if (p_==NULL) throw ...
return p_->elem_;
}

inline LinkedListIterator::LinkedListIterator(Node* p)
: p_(p)
{ }

inline LinkedListIterator LinkedList::begin()
{
return first_;
}

inline LinkedListIterator LinkedList::end()
{
return NULL;
}

Pour conclure: la liste chaînée gère deux sortes de données différentes. On trouve d'un côté les valeurs des éléments qui sont stockés dans la liste chaînée. Ces valeurs sont sous la responsabilité de l'utilisateur de la liste et seulement de l'utilisateur. La liste elle-même ne fera rien par exemple pour empêcher à un utilisateur de donner la valeur 5 au troisième élément, même si ça n'a pas de sens dans le contexte de cet utilisateur. On trouve de l'autre côté les données d'implémentation de la liste (pointeurs next, etc.), dont les valeurs sont sous la responsabilité de la liste et seulement de la liste, laquelle ne donne aux utilisateurs aucun accès (que ce soit en lecture ou en écriture) aux divers pointeurs qui composent son implémentation.

Ainsi, les seules méthodes get()/set() présentes sont là pour permettre la modification des éléments de la liste chaînée, mais ne permettent absolument pas la modification des données d'implémentation de la liste. Et la liste chaînée ayant complètement masqué son implémentation, elle peut donner des garanties très fortes concernant cette implémentation (dans le cas d'une liste doublement chaînée par exemple, la garantie pourrait être qu'il y a pour chaque pointeur avant, un pointeur arrière dans le Node suivant).

Nous avons donc vu un exemple dans lequel les valeurs de certaines des données d'une classe étaient sous la responsabilité des utilisateurs de la classe (et la classe a besoin d'exposer des méthodes get()/set() pour ces données) mais dans lequel les données contrôlées uniquement par la classe ne sont pas nécéssairement accessibles par des méthodes get()/set().

Note: le but de cet exemple n'était pas de vous montrer comment écrire une classe de liste chaînée. Et d'abord, vous ne devriez pas "pondre" votre propre classe liste, vous devriez plutôt utiliser l'une des classes de type "conteneur standard" fournie avec votre compilateur. La meilleure solution est d'utiliser l'une des classes conteneurs du standard C++ , par exemple la classe template list<T>.

[ Haut | Bas | Rechercher ]


[13.11] How can I overload the prefix and postfix forms of operators ++ and --?

Via a dummy parameter.

Since the prefix and postfix ++ operators can have two definitions, the C++ language gives us two different signatures. Both are called operator++(), but the prefix version takes no parameters and the postfix version takes a dummy int. (Although this discussion revolves around the ++ operator, the -- operator is completely symmetric, and all the rules and guidelines that apply to one also apply to the other.)

class Number {
public:
	Number& operator++ ();    // prefix ++
	Number  operator++ (int); // postfix ++
};

Note the different return types: the prefix version returns by reference, the postfix version by value. If that's not immediately obvious to you, it should be after you see the definitions (and after you remember that y = x++ and y = ++x set y to different things).

Number& Number::operator++ ()
{
	...
	return *this;
}

Number Number::operator++ (int)
{
	Number ans = *this;
	++(*this);  // or just call operator++()
	return ans;
}

The other option for the postfix version is to return nothing:

class Number {
public:
	Number& operator++ ();
	void    operator++ (int);
};

Number& Number::operator++ ()
{
	...
	return *this;
}

void Number::operator++ (int)
{
	++(*this);  // or just call operator++()
}

However you must *not* make the postfix version return the 'this' object by reference; you have been warned.

Here's how you use these operators:

Number x = /* ... */;
++x;  // calls Number::operator++(), i.e., calls x.operator++()
x++;  // calls Number::operator++(int), i.e., calls x.operator++(0)

Assuming the return types are not 'void', you can use them in larger expressions:

Number x = /* ... */;
Number y = ++x;  // y will be the new value of x
Number z = x++;  // z will be the old value of x

[ Haut | Bas | Rechercher ]


[13.12] Which is more efficient: i++ or ++i?
++i is sometimes faster than, and is never slower than, i++.

For intrinsic types like int, it doesn't matter: ++i and i++ are the same speed. For class types like iterators or the previous FAQ's Number class, ++i very well might be faster than i++ since the latter might make a copy of the this object.

The overhead of i++, if it is there at all, won't probably make any practical difference unless your app is CPU bound. For example, if your app spends most of its time waiting for someone to click a mouse, doing disk I/O, network I/O, or database queries, then it won't hurt your performance to waste a few CPU cycles. However it's just as easy to type ++i as i++, so why not use the former unless you actually need the old value of i.

So if you're writing i++ as a statement rather than as part of a larger expression, why not just write ++i instead? You never lose anything, and you sometimes gain something. Old line C programmers are used to writing i++ instead of ++i. E.g., they'll say, for (i = 0; i < 10; i++) ... . Since this uses i++ as a statement, not as a part of a larger expression, then you might want to use ++i instead. For symmetry, I personally advocate that style even when it doesn't improve speed, e.g., for intrinsic types and for class types with postfix operators that return void.

Obviously when i++ appears as a part of a larger expression, that's different: it's being used because it's the only logically correct solution, not because it's an old habit you picked up while programming in C.

[ Haut | Bas | Rechercher ]


E-mail Marshall Cline Ecrire à l'auteur, au traducteur, ou en savoir plus sur la traduction.
C++ FAQ Lite fr | Table des matières | Index | A propos de l'auteur | © | Téléchargez votre propre copie ]
Dernière révision Sun Apr 13 23:54:30 PDT 2003