Opérateurs membres ou amis

Lorsqu’on définit un opérateur pour une classe, on ne sait pas forcément très bien comment le déclarer. En particulier, faut-il en faire une fonction membre, ou une fonction amie ? Et quels arguments doivent être passés en référence ?

Il n’y a pas de réponse générale à ce problème, mais un certain nombre de règles simples que l’on peut suivre, quoiqu’elles n’aient rien d’obligatoire.

Si l’opérateur demande parmi ses arguments une valeur modifiable (lvalue), il est préférable d’en faire une méthode, afin d’éviter des écritures étranges. C’est ce que nous avons fait pour l’opérateur d’affectation, dont le premier argument est une valeur modifiable. En effet, si l’on écrivait :

fraction& operator=(fraction& f1,  fraction f2)// bizarre...{     f1.num = f2.num;     f1.den = f2.den;     return f1;}

alors l’écriture suivante :

fraction f(2/5);4 = f;

serait parfaitement licite : elle équivaudrait à créer un objet temporaire de valeur 4/1, y recopier 2/5, puis à le détruire : il n’y aurait donc aucun effet. Le moins que l’on en puisse dire c’est que ce n’est guère naturel. Si l’on a par contre défini un tel opérateur comme un membre (comme nous l’avons fait pour la classe matrice précédemment), cette écriture devient interdite parce que le compilateur ne fait pas de conversion de type pour les instances qui appellent un membre.

Inversement, si l’on avait écrit l’opérateur d’addition ainsi :

class fraction {     // ......     fraction operator+(fraction f)         {             f.num = num*f.den + den*f.num;             f.den *= den;             return f;         }     }

on pourrait ajouter 1 à 2/5 mais pas 2/5 à 1.

Entre ces deux comportements, il faut donc choisir. Dans certains cas, les deux semblent équivalents. À ce moment il est préférable en général d’utiliser des membres, qui sont plus faciles à écrire, puisqu’on a accès directement aux champs.

Pour les fonctions qui ne sont pas des opérateurs, on choisit selon la syntaxe souhaitée. Par exemple, l’inversion d’une matrice est plus agréable écrite inv(M) que M.inv() : on en fera plutôt une amie. Par contre, l’élévation à une puissance entière est peut-être plus claire sous la forme M.pow(i) que pow(M, i) : on en fera un membre.

Pour ce qui est du type des arguments et du résultat, il faut choisir entre une référence et un élément normal. Pour les arguments, il suffit de faire comme pour toute fonction : si l’argument est petit et a des constructeurs simples, on peut le passer par valeur. Si par contre la fonction ne modifie pas l’argument et que celui-ci est gros, ou a des constructeurs compliqués (exigeant par exemple une allocation de bloc mémoire), utiliser une référence. Quant au résultat, il est préférable en général de le passer par valeur. Un résultat référence est en effet dangereux. Cependant, on peut passer un tel résultat référence lorsque la référence est en fait un des arguments référence ou pointeur (y compris this s’il y a lieu) : c’est le cas des affectations, et aussi de << et >> pour les fichiers de sortie et d’entrée (voir chapitre 9).

On peut aussi renvoyer une référence sur un argument passé par valeur, parce que le destructeur afférent n’est appelé qu’après la fin complète du calcul de l’expression courante. Par exemple, si l’on écrit :

class exemple {     // .....     exemple(exemple&);     // constructeur de copie     ~exemple();            // destructeur     exemple& operator+=(exemple ex)             {                  // affectation-addition             // ... additionner...              return *this;          }     };exemple& operator+(exemple ex1, exemple&  ex2)         {    return ex1+= ex2; }    // addition         main(){     exemple exmpl1, exmpl2;     exemple exmpl3 = exmpl1 + exmpl2;     // ....}

alors le programme sera développé comme ceci :

main()           // écriture  développée{     exmpl1.exemple::exemple();       // constr. par défaut     exmpl2.exemple::exemple();       // idem     // début de l’addition : création de ex1     ex1.exemple::exemple(exmpl1);    // constr. de copie     // passage dans la fonction en ligne operator+     ex1.exemple::operator+=(exmpl2); // addition     // retour du résultat ex1 dans exmpl3     exmpl3.exemple::exemple(ex1);    // constr. de copie     // addition terminée     ex1.exemple::~exemple();         // appel du destructeur     // ......}

On voit que le destructeur pour l’argument provisoire ex1 est appelé après que celui-ci ait été copié dans le résultat de l’addition exmpl3. De ce fait l’opération se déroule correctement, ce qui n’aurait pas été le cas autrement. L’ordre des appels peut être vérifié en regardant les imbrications explicites dans les opérateurs de fonction. Ainsi l’addition équivaut à :

exmpl3.exemple::exemple(operator+(exmpl1,  exempl2));

ce qui explique pourquoi le destructeur est appelé en dernier.

De telles considérations sont complexes, et pour un gain parfois faible. Dans le doute, n’utilisez pas de références.

Précédent Précédent Sommaire Sommaire Suivant Suivant