Lorsquon 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 ny a pas de réponse générale à ce problème, mais un certain nombre de règles simples que lon peut suivre, quoiquelles naient rien dobligatoire.
Si lopérateur demande parmi ses arguments une valeur modifiable (lvalue), il est préférable den faire une méthode, afin déviter des écritures étranges. Cest ce que nous avons fait pour lopérateur daffectation, dont le premier argument est une valeur modifiable. En effet, si lon é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 ny aurait donc aucun effet. Le moins que lon en puisse dire cest que ce nest guère naturel. Si lon a par contre défini un tel opérateur comme un membre (comme nous lavons 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 lon avait écrit lopérateur daddition 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 dutiliser des membres, qui sont plus faciles à écrire, puisquon 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, linversion dune 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 largument est petit et a des constructeurs simples, on peut le passer par valeur. Si par contre la fonction ne modifie pas largument 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
sil y a lieu) : cest le cas des affectations, et aussi de <<
et >>
pour les fichiers de sortie et dentrée (voir chapitre 9).
On peut aussi renvoyer une référence sur un argument passé par valeur, parce que le destructeur afférent nest appelé quaprès la fin complète du calcul de lexpression courante. Par exemple, si lon é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 laddition : 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 largument provisoire ex1
est appelé après que celui-ci ait été copié dans le résultat de laddition exmpl3
. De ce fait lopération se déroule correctement, ce qui naurait pas été le cas autrement. Lordre des appels peut être vérifié en regardant les imbrications explicites dans les opérateurs de fonction. Ainsi laddition é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, nutilisez pas de références.
Précédent | Sommaire | Suivant |