Nombre complexe et C++

by dinosaure

Mes collègues de ma classe ont fait une blague pour le DM. En effet, il m’ont fait croire qu’il y avait un exercice en plus à rendre. Néanmoins, même si le travail m’a demander plus de temps, j’ai apprécier l’exercice. J’ai donc appliquer cette exercice dans un programme informatique. Il s’agit donc ici de faire quoi ? Eh bien d’implémenter la notion de complexe en C++ grâce aux classes et à la surcharge des opérateurs. Notez tout de même que la STL propose déjà une classe complex. Mais là n’est pas le but d’être laxiste, il faut bien maîtriser les notions de surcharge d’opérateurs et du mot clé friend. On doit être capable, non seulement, de faire des opérations arithmétiques sur les complexes mais permettre à l’utilisateur d’accéder aux propriétés de notre complexe (c’est là où intervient le mot clé friend).

Il faut bien comprendre que cette exercice n’a pas pour vocation de vous apprendre des maths (si c’est ce que vous chercher, vous prenez votre livre et vous faîtes les exercices). Il permet pas non plus d’apprendre des notions complexes d’algorithmes, il sert juste à bien utiliser les notions que nous propose le langage C++ et ce n’est pas à négliger car un bon programmeur, c’est quelqu’un qui maîtrise avant tout son langage. Donc les notions que je vais expliquer ici ne tiennent pas à être difficile et où il ne vous faudrait pas un papier et un crayon à côté pour comprendre, il suffit de lire et surtout (surtout !) d’appliquer le plus possible. De plus, il faut porter une réflexion non pas sur le fonctionnement de notre classe (par exemple, comment additionner 2 complexes) mais sur l’aperçu extérieur de notre classe pour que son utilisation soit spontanée. Comment faire ? Un complexe s’utilise dans les mathématiques, il est donc primordial de définir tout les opérateurs arithmétiques possibles pour notre classe Complexe. On pourrait aussi ce rapprocher de l’écriture mathématique notamment avec Im() et Re(). Il arrive aussi que l’utilisateur préfère utiliser les coordonnées polaires, il nous faut donc une fonction capable de passer d’une coordonnée polaire à une coordonnée cartésienne. Enfin, il peut être intéressant de proposer des outils de conversions d’un objet Complexe à une structure comme Coordinate (struct Coordinate { float x, y; }; par exemple).

Friend

Pour bien comprendre ce qui va suivre, il faut comprendre la notion de friend dans une classe. Ce mot clé permet d’offrir à une autre classe ou à une fonction des privilèges d’accès en rapport avec notre classe de base. Comme vous le savez bien, les propriétés d’une classe doivent être privées pour respecter l’encapsulation. Néanmoins, il est parfois nécessaire d’accéder aux propriétés d’une classe. Une première méthode serait les accesseurs (getReal) qui sont des fonctions membre de la classe. Néanmoins, dans notre cas des complexes, on voudrait ce rapprocher le plus possible de l’écriture mathématique:


Cette écriture convient à une fonction libre dont le prototype serait float Re(const Complexe &a) pour la partie réel et c’est la même chose pour Im(). Néanmoins, comment accéder à la partie réel ou à la partie imaginaire de notre complexe qui sont des propriétés privées ? C’est à l’aide du mot clé friend. Dans la définition de notre classe, nous allons spécifier que la fonction Re() et Im() sont des fonctions amies de notre classe Complexe pour qu’ils aient accès aux propriétés mon_complexe.real et mon_complexe.imaginary. Il faut donc définir notre classe Complexe comme ceci:

class Complexe
{
    friend float Re(const Complexe &a);
    friend float Im(const Complexe &a);

    private:
        float   real;
        float   imaginary;
};

L’accès aux propriétés de la classe Complexe sera ainsi toléré pour les fonctions Re() et Im(). Il faut tout de même préciser certaines spécificités sur le mot clé friend. Il faut savoir que les fonctions/classes friend ne brise pas l’encapsulation. Il s’avère parfois plus judicieux d’utiliser le mot clé friend que les accesseurs. Notre utilité ici ce trouve comme une alternative syntaxique. De plus, en utilisant le mot clé const dans notre prototype, on oblige la fonction de n’être que en lecture seul sur les propriétés de notre classe. La modification de la partie réel de notre complexe indépendamment de la partie imaginaire n’est pas possible dans notre cas. Ensuite, la particularité du mot clé friend, c’est que les privilèges qui y sont associés ne sont pas hérité, transitive et pas réciproque ce qui assure encore une fois l’encapsulation. Mais dans notre cas, cela ne nous concerne pas vraiment. Les fonctions Re() et Im() ont juste à retourner respectivement a.real et a.imaginary. Maintenant, on peut créer un objet Complexe et respecter la notation mathématique pour la partie réel ou imaginaire de notre complexe.

Opérateur

On vous a sûrement appris à définir les opérateurs d’une classe comme étant membre de cette dite-classe. C’est à dire, que vous avez surcharger les opérateurs comme + directement dans une fonction membre de la classe. C’est bien ça marche, mais pas tout le temps. Pour ce faire, il faut comprendre un peu comment ça fonctionne. Affecter l’opérateur + à une fonction membre de la classe en définissant explicitement (et c’est normal) le type à rentrer en paramètre vous limite dans ce que vous avez écrit. Si on écrit ce type de fonction: Complexe Complexe::operator+(const Complexe &a) cela vous limitera qu’à additionner des complexes avec des complexes. Et oui, car le seul type accepter pour ce qui est de l’addition dans votre classe est le type objet Complexe. Mais vous me direz, alors on surcharge une nouvelle fois l’opérateur + pour int, float et whatever ? Oui, c’est une solution, mais surcharger l’opérateur + plusieurs fois pour une question de type, c’est une baisse de votre productivité et il y a un moyen plus rapide pour le faire: le transtypage. Votre compilateur (du moins g++) est intelligent et il sait ce qu’il faut faire quand on lui donne les moyens de le faire. Si on regarde de plus près la surcharge de l’opérateur + avec notre classe Complexe, on obtient ça:

Complexe a = Complexe(1, 1) + Complexe(2, 2);
// On a défini le constructeur de Complexe
Complexe a = Complexe(1, 1).operator+(Complexe(2, 2));
// Cette ligne équivaut à la première

Pour l’instant, tout va bien, on ajoute un Complexe à un autre Complexe ce qui correspond à notre prototype Complexe Complexe::operator+(const Complexe &a), on aura créer un constructeur acceptant 2 paramètres du type float pour définir respectivement les propriétés real et imaginaru de notre objet. Puisqu’on a surcharger l’opérateur + dans une fonction membre de notre classe, il est logique de voir apparaitre Complexe(1, 1).operator+(Complexe(2, 2)). C’est comme une fonction membre normal qu’on appellerait de la même façon Mon_Objet.ma_methode(). Néanmoins, si nous voulions par exemple tout simplement ajouter 5 ? Comment nous pourrions faire ? C’est une valeur de type int et à aucun moment on a surcharger l’opérateur + pour une valeur de type int. Comme il est expliqué plus haut, on pourrait le faire, mais pour float, double, etc ? On va utiliser le transtypage est donner le moyen à notre compilateur de convertir une valeur int en un Complexe, ainsi l’opération pourra ce faire et on aura pas à surcharger encore une fois l’opérateur +:

Complexe::Complexe(const float &a) : real(a), imaginary(0) { }
Complexe a = Complexe(1, 1) + 5;
Complexe a = Complexe(1, 1).operator+(Complexe(5));

Ici, on a donné le moyen au compilateur de convertir un float en un complexe en définissant le constructeur Complexe(const float &a). Mais nous ne voulions pas déjà utiliser un int ? On peut très bien le faire, le problème, c’est que l’utilisation des complexes demande souvent des nombre avec des virgules. Nous pourrions faire un constructeur pour les int, le problème, c’est que là aussi le compilateur est intelligent et peut passer d’un int à un float implicitement. Sauf que passer d’un float à un int équivaut à une perte de donnée car imaginons que nous définissons qu’un constructeur pour les int pour notre classe Complexe, si on fait ce genre d’opération Complexe a = Complexe(1, 1) + 5.6. Au moment du transtypage (lors de l’appelle de notre constructeur), 5.6 va ce transformer 5 puisque je le rappelle que les int sont des nombres entiers. Le choix d’un float (ou d’un double) est donc justifié et la conversion implicite du int au float ce fera sans perte. Au final, notre calcul ressemblera à ça:

Complexe a = Complexe(1, 1).operator+(Complexe(float(5)));

On a donné le moyen à notre compilateur de traiter une valeur objet Complexe avec un int ou un float juste en définissant un nouveau constructeur. C’est un constructeur de transtypage. Il est important surtout dans notre cas pour une utilisation, qui, tournée vers l’utilisateur, soit spontanée. Mais cela ne résout pas totalement le problème car on ne peut pas mettre l’élément de gauche de notre opération entant que argument à notre fonction membre. C’est à dire que l’appelant de l’opérateur + ne peut pas être insérer comme argument à la méthode surchargeant notre opérateurs. Prenons l’exemple de notre 5 encore une fois mais cette fois, on va faire:

Complexe a = 5 + Complexe(1, 1);
Complexe a = 5.operator+(Complexe(1, 1);

Si on suit toujours la même logique, on obtiendra ce code. Néanmoins, cela va générer une erreur. Pourquoi ? Car il est aucunement défini dans le type int l’opérateur + acceptant comme argument un Complexe. Vous ne pouvez donc pas écrire ce code sous faute d’une erreur car personne n’a surcharger l’opérateur + pour int en spécifiant un argument de type objet Complexe. Il y a néanmoins, plusieurs moyens mis à notre disposition. L’un d’eux serait de créer un opérateur de transtypage (et pas un constructeur) dans notre classe Complexe pour que le compilateur fasse une conversion implicite de l’objet Complexe au type int. Mais vous vous en doutez, il y a une perte de données encore une fois, surtout qu’ici, on voudra un Complexe entant que résultat. Puis votre compilateur n’est pas super intelligent et il ne pense pas faire ceci: Complexe a = Complexe(5).operator+(Complexe(1, 1));. Ce qui est normal. Alors comment faut il faire ? Il y a une autre solution, c’est de surcharger l’opérateur + par exemple mais dans une fonction libre qui prendra en compte alors l’élément de gauche de l’addition (ce que nous voulions faire) et l’élément de droite. Cette fonction aura la particularité de renvoyer un Complexe ce qui assurera son unicité (et ce qui permettra au compilateur de choisir cette fonction quand nous voudrions avoir comme résultat un complexe). Ensuite, le compilateur grâce aux outils qu’on lui propose (notamment le constructeur de transtypage) pourra faire l’addition en faisant une conversion implicite de 5 en un complexe. Mais comment faire là aussi. Eh bien, il y a encore plusieurs méthodes. Une méthode général et une autre qui ce destine à notre implémentation. La méthode général serait de définir ces fonctions libres comme friend de notre classe pour quelles puissent avoir accès aux propriétés de notre classe (on vous conseillera toujours cette méthode dans ce cas). Cela ressemblerait à ça:

class Complexe
{
    friend Complexe operator+(const Complexe &a, const Complexe &b);

    private:
        float   real;
        float   imaginary;
};

Sauf que nous, nous avons déjà spécifier des fonctions qui ont accès aux propriétés de notre classe Complexe et en plus, ces fonctions les retournent. On a donc accès (seulement en lecture, hein) aux propriétés de notre classe par le biais des fonctions Re() et Im(). Il n’est donc pas nécessaire de surcharger nos opérateurs par le biais de fonctions friend à notre classe Complexe et de faire appelle à Re() et à Im() pour connaître les propriétés des objets qu’on manipulera. Le prototype de nos fonctions ce fera donc hors de notre classe (ces fonctions dépendront quand même de notre classe car les arguments et l’objet retour sont tous du type objet Complexe):

class Complexe
{
    // ...
};

Complexe operator+(const Complexe &a, const Complexe &b);

L’addition ne ce fera plus de la même manière maintenant. Puisqu’on a défini la surcharge de notre opérateur + dans une fonction libre, on a la possibilité de prendre en compte non seulement l’objet de droite mais aussi celui de gauche. Puisqu’on spécifie explicitement le type de retour étant Complexe, dès qu’il s’agira d’obtenir un résultat pour l’assigner à un complexe, le compilateur saura qu’il faut utiliser notre fonction (et oui, il est intelligent). En plus, on a créer un constructeur de transtypage ce qui nous donne la possibilité d’additionner un int (float, double, whatever) à nos complexes car, là aussi, notre compilateur est intelligent est va faire les conversions nécessaires pour que tout ce passe bien. Quand on ajoutera un complexe et un nombre par exemple et que nous voudrions obtenir en retour un nombre complexe, on fera appelle à cette fonction de cette façon:

Complexe a = 5 + Complexe(1, 1);
Complexe a = operator+(Complexe(5), Complexe(1, 1));

La particularité chez nous, c’est que puisqu’on a offert un accès en lecture seul aux propriétés de notre classe (avec les fonctions Re() et Im()), il n’est pas nécessaire de définir nos fonctions surchargeant les opérateurs comme amies de notre classe puisqu’il suffit de faire appelle aux dites-fonctions pour créer un nouveau complexe qui résultera de l’addition de 2 complexes (cf. votre cours de maths). On peut s’amuser à ajouter des nouvelles fonctions comme obtenir le conjugué d’un complexe, le module, etc … Mais ça, c’est dans vos corde (d’ailleurs vous pouvez utiliser les fonctions friend dans ce cas là aussi pour faire Module(z) par exemple même si, comme pour les opérateurs, ce n’est pas indéniable). Mais il s’agit de proposer d’autres outils à notre programmeur. L’un d’eux est de convertir une coordonnée polaire en une coordonné cartésienne. Pour ce faire, je vous renvoie à votre cours de maths. On pourrait penser créer un autre constructeur pour faire la conversion directement au lieu de passer par une fonction. Néanmoins, il y a ambiguïté entre notre constructeur de base Complexe(const float &r, const float &i) et le supposé constructeur pour les coordonnées polaires Complexe(const float &m, const float &t). Même si, sur l’un, il s’agit de donner un angle (en radian ou en degré) et sur l’autre un module, tout les deux (les arguments) sont considéré comme des float. Ainsi, l’utilisation d’une fonction libre et justifié:

Complexe Polar(const float &m, const float &t);

Pour une meilleur organisation, on peut très bien utilisé les namespace mais bon, ce serait vous expliquer une nouvelle chose qui n’a pas réellement son intérêt ici. Non, maintenant, il faut parler des opérateurs de transtypage. En effet, il peut être nécessaire que votre complexe devienne un autre objet ou une autre structure. C’est là où ils interviennent. Normalement, le constructeur (à ce que j’ai pu lire mais je n’ai pas de confirmation) de transtypage est plus rapide que l’opérateur de transtypage. Néanmoins, on peut pas ce permettre de dire à notre programmeur: voilà, on a une super classe sauf que pour la convertir en un autre objet, tu dois définir dans ton objet un constructeur de transtypage. Il faut toujours penser extérieur. Il nous faut donc créer un opérateur de transtypage mais comment ça marche ? C’est très simple, la définition d’une fonction pour l’opérateur de transtypage est similaire que pour la surcharge d’un opérateur. Mais c’est pas exactement ça:

Complexe::operator Coordinate() const;

Il y a le mot clé operator et le nom de notre fonction n’est autre que le type de retour (ici Coordinate pour struct Coordinate { float x, y; };). Comme vous le voyez, il n’y a pas besoin de spécifier le type de retour car il équivaut au nom de notre fonction (pratique l’ami). On notera aussi qu’il n’y a pas la présence d’arguments car, en effet, on va agir sur l’appelant de cette fonction, c’est à dire this. Bien entendu, ceci est dans le cas où cet opérateur de transtypage est défini par le biais d’une fonction membre de notre classe. Dans le cas d’une fonction libre, il faudra spécifier un argument du type Complexe. Pour ceux qui ne le savent pas, this est un pointeur par défaut qui désigne l’objet en lui-même. C’est un pointeur non modifiable, ce qui est logique (merci la FAQ C++). Dans cette fonction, il suffira de créer un objet type Coordinate dont x vaut Re((*this)) (notez le *, en effet, this est un pointeur) et y vaut Im((*this)). Notez encore une fois que ici, j’utilise les fonctions friend Re() et Im(). Cet opérateur de transtypage peut donc être défini par le biais d’une fonction libre.


Bon, vous savez les grandes lignes et vous pouvez déjà vous appliquer à faire une classe Complexe. Il faut savoir que les notions du type surcharge d’opérateur dans des fonctions libres est un cas à ne pas appliquer à toutes vos classes. En effet, ici, c’était nécessaire pour pouvoir utiliser des types natifs (comme int ou float) sans surcharger encore une fois selon le type. Néanmoins, vous ferez sûrement des classes qui n’auront pas nécessairement besoin qu’on leurs ajoute des nombres. On peut par exemple restreindre le programmeur à n’additionner que les objets d’une classe entre eux (sous faute d’une erreur de compilation sinon). J’aborde ce problème car, pour les sources que j’ai observé (dans les cours débutants), rien ne soulever le problème, et quant on pouvez (enfin !) voir des fonctions libres, on nous expliquer par pourquoi. En espérant que ce petit article à fait la lumière sur les opérateurs. Il y aura aussi peut être des update pour appliquer cette classe à un problème mathématique !