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

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

56. Surcharge des opérateurs

Les sujets abordés dans ce chapitre s'appliquent également pour la plupart aux classes. La principale différence est que le comportement de l'opération d'affectation opAssign() ne peut pas être surchargé pour les classes.
La surcharge des opérateurs met en œuvre de nombreux concepts dont certains seront abordés plus loin dans ce livre (les modèles, auto ref, etc.). Pour cette raison, vous pourriez trouver ce chapitre plus difficile à suivre que les précédents.
La surcharge des opérateurs permet de définir comment des types définis par l'utilisateur se comportent lorsqu'ils sont utilisés avec des opérateurs. Dans ce contexte, surcharger signifie fournir une définition d'un opérateur pour un typespécifique.

Nous avons vu comment définir des structures et leurs fonctions membres dans les chapitres précédents. Par exemple, nous avons défini la fonction incrementer() afin d'ajouter des objets Duree à des objets MomentDeLaJournee. Voici les deux structures des chapitres précédents, avec seulement les parties qui sont importantes pour ce chapitre :

 
Sélectionnez
struct Duree
{
    int minute;
}
 
struct MomentDeLaJournee
{
    int heure;
    int minute;
           
    void incrementer(in Duree duree)
    {
        minute += duree.minute;
        heure += minute / 60;
        minute %= 60;
        heure %= 24;
    }
}
 
void main()
{
    auto momentDuRepas = MomentDeLaJournee(12, 0);
    momentDuRepas.incrementer(Duree(10));
}

L'avantage des fonctions membres est de pouvoir définir les opérations d'un type avec les variables membres de ce type.
Malgré leur avantage, les fonctions membres peuvent être vues comme limitées comparées aux opérations sur les types fondamentaux. Après tout, les types fondamentaux peuvent être utilisés avec des opérateurs :

 
Sélectionnez
int masse = 50;
masse += 10; // avec un opérateur

Selon ce que l'on a déjà vu jusqu'à présent, de telles opérations ne peuvent être rendues possibles que par des fonctions membres pour les types utilisateur :

 
Sélectionnez
auto momentDuRepas = MomentDeLaJournee(12, 0);
momentDuRepas.incrementer(Duree(10));        // avec une fonction membre

La surcharge des opérateurs permet d'utiliser également les opérateurs avec les structures et les classes. Par exemple, en partant du principe que l'opérateur += est défini pour MomentDeLaJournee, l'opération ci-avant peut-être écrite de la même manière que pour un type fondamental :

 
Sélectionnez
momentDuRepas += Duree(10); // avec un opérateur (même pour une structure)

Avant de rentrer dans les détails de la surcharge des opérateurs, voyons tout d'abord comment la ligne ci-avant serait rendue possible pour MomentDeLaJournee. Ce qu'il nous faut, c'est renommer la fonction incrementer et lui donner le nom spécial opOpAssign(string op) et spécifier que cette définition vaut pour le caractère +. Comme cela sera expliqué ci-après, cette définition correspond à l'opérateur +=.

La définition de cette fonction membre ne ressemble pas à celles que nous avons vues jusqu'à présent. C'est parce que opOpAssign est une fonction modèle. Puisque nous verrons les modèles bien plus tard dans ce livre, je vous demande pour le moment de bien vouloir accepter telle quelle la syntaxe de surcharge des opérateurs :

 
Sélectionnez
struct MomentDeLaJournee
{
    // ...
    ref MomentDeLaJournee opOpAssign(string op)(in Duree duree) //(1)
        if (op == "+")                                          //(2)
    {
        minute += duree.minute;
        heure += minute / 60;
        minute %= 60;
        heure %= 24;
 
        return this;
    }
}

La définition du modèle consiste en deux parties :

  1. opOpAssign(string op) : cette partie doit être écrite telle quelle et devrait être acceptée comme étant le nom de la fonction. Nous verrons ci-après qu'il existe d'autres fonctions membres en plus de opOpAssign :
  2. if (op == "+") : opOpAssign sert pour plus d'une surcharge d'opérateur. « + » spécifie qu'il s'agit de la surcharge d'opérateur qui correspond au caractère +. Cette syntaxe est une contrainte de modèle, qui sera abordée dans des chapitres ultérieurs.

Veuillez par ailleurs noter que type de retour est différent de celui de la fonction membre incrementer : ce n'est plus void. Nous discuterons des types de retours des opérateurs ci-après.

En coulisse, le compilateur remplace les utilisations de l'opérateur += par des appels à la fonction membre opOpAssign!"+" :

 
Sélectionnez
momentDuRepas += Duree(10);
 
// la ligne suivante est l'équivalent de la précédente
momentDuRepas.opOpAssign!"+"(Duree(10));

La partie !« + » qui est après opOpAssign spécifie que cet appel vaut pour la définition de l'opérateur avec le caractère +. Nous verrons également cette syntaxe dans le chapitre sur les modèles.
Notez que l'opérateur qui correspond à += est défini par « + » et pas par « += ». Le mot Assign (affectation en anglais) dans opOpAssign() implique déjà que c'est un opérateur d'affectation.
Pouvoir définir le comportement des opérateurs implique une responsabilité : le programmeur doit respecter les attentes de son public. Comme exemple extrême, l'opérateur précédent pourrait avoir été défini pour décrémenter du temps au lieu d'en incrémenter. Néanmoins, les gens qui liraient le code s'attendraient naturellement à ce que l'opérateur += incrémente la valeur qui lui est passée.
Dans une certaine mesure, le type de retour d'un opérateur peut aussi être choisi librement. Cependant, les attentes générales doivent également être respectées pour les types de retours.
Gardez à l'esprit que les opérateurs qui ne se comportent pas naturellement sèment la confusion et sont cause de bogues.

56-1. Opérateurs surchargeables

Il y a différents types d'opérateurs qui peuvent être surchargés.

56-1-1. Opérateurs unaires

Un opérateur qui ne prend qu'un seul opérande est appelé un opérateur unaire :

 
Sélectionnez
++masse;

++ est un opérateur unaire parce qu'il n'agit que sur une seule variable.

Les opérateurs unaires sont définis par des fonctions membres appelées opUnary. OpUnary ne prend aucun paramètre parce qu'elle n'utilise que le seul objet sur lequel l'opérateur est exécuté.
Les opérateurs unaires qui peuvent être surchargés et leur chaîne de caractères correspondante sont les suivants :

Opérateur

Description

Chaîne de caractères

-objet

Négatif (opposé numérique)

« - »

+objet

Même valeur (ou une copie)

« + »

~objet

Négation bit à bit

""

*objet

Accès à la valeur pointée

« * »

++objet

incrément

« ++ »

--objet

décrément

« -- »

Par exemple, l'opérateur ++ pour Duree peut-être défini comme ceci :

 
Sélectionnez
struct Duree
{
    int minute;
 
    ref Duree opUnary(string op)()
        if (op == "++")
    {
        ++minute;
        return this;
    }
}

Notez que le type de retour de l'opérateur est également marqué comme ref. Ceci sera expliqué plus tard. Les objets Duree peuvent maintenant être incrémentés avec ++ :

 
Sélectionnez
auto duree = Duree(20);
++duree;

Les opérateurs de postincrément et de postdécrément ne peuvent pas être surchargés. Les cas d'utilisation d'objet++ et objet-- sont gérés automatiquement, pris en charge par le compilateur en sauvegardant la valeur précédente de l'objet. Par exemple, le compilateur applique l'équivalent du code suivant pour le postincrément :

 
Sélectionnez
/* La valeur précédente est copiée par le compilateur 
automatiquement */
Duree __valeurPrecedente__ = duree;
 
/* L'opérateur ++ est appelé */
++duree;
 
/* Puis __valeurPrecedente__ est retournée comme valeur de retour
 * de l'opération de postincrément */

Contrairement à certains autres langages, la copie à l'intérieur du postincrément n'a aucun coût en D si la valeur de retour du postincrément n'est pas utilisée. C'est parce que le compilateur remplace ces expressions de postincrément par leur homologue préincrément :

 
Sélectionnez
/* La valeur de l'expression n'est pas utilisée ci-après, le 
   seul effet de l'expression est d'incrémenter 'i'. */
i++;

Parce ce que la précédente valeur de i n'est pas utilisée dans le code ci-avant, le compilateur remplace l'expression par la suivante :

 
Sélectionnez
/* L'expression qui est réellement utilisée par le compilateur: */
++i;

Par ailleurs, si une surcharge de opBinary supporte l'utilisation de duree += 1, alors la surcharge de opUnary n'est pas nécessaire pour ++duree et duree++. Dans ce cas, le compilateur utilise duree += 1 en coulisse. De la même manière, la surcharge de duree -= 1 couvre les utilisations de --duree et de duree--.

56-1-2. Opérateurs binaires

Un opérateur qui prend deux opérandes et appelé opérateur binaire :

 
Sélectionnez
masseTotale = masseBoite + masseChocolat;

La ligne ci-dessus contient deux opérateurs binaires différents : l'opérateur +, qui additionne les valeurs des opérandes qui sont sur ses deux côtés, et l'opérateur = qui affecte la valeur de son opérande de droite à son opérande de gauche.
La colonne de droite ci-après décrit la catégorie de chaque opérateur. Ceux marqués comme « = » affectent une valeur à l'objet à leur gauche.

Opérateur

Description

Nom de fonction

Nom de fonction côté droit

Catégorie

+

Addition

opBinary

opBinaryRight

arithmétique

-

Soustraction

opBinary

opBinaryRight

arithmétique

*

Multiplication

opBinary

opBinaryRight

arithmétique

/

Division

opBinary

opBinaryRight

arithmétique

%

Reste

opBinary

opBinaryRight

arithmétique

^^

Puissance

opBinary

opBinaryRight

arithmétique

&

Et bit à bit

opBinary

opBinaryRight

bit à bit

|

Ou bit à bit

opBinary

opBinaryRight

bit à bit

^

Ou exclusif bit à bit

opBinary

opBinaryRight

bit à bit

<<

Décalage à gauche

opBinary

opBinaryRight

bit à bit

>

Décalage à droite

opBinary

opBinaryRight

bit à bit

>>

Décalage à droite non signé

opBinary

opBinaryRight

bit à bit

~

Concaténation

opBinary

opBinaryRight

 

in

Est contenu dans

opBinary

opBinaryRight

 

==

Est égal à

opEquals

 

Égalité

!=

Est différent de

opEquals

 

Égalité

<

Est avant

opCmp

 

Ordre

<=

N'est pas après

opCmp

 

Ordre

 

Est après

opCmp

 

Ordre

=

N'est pas avant

opCmp

 

Ordre

=

Affectation

opAssign

 

=

+=

Additionne et affecte

opOpAssign

 

=

-=

Soustrait et affecte

opOpAssign

 

=

*=

Multiplie et affecte

opOpAssign

 

=

/=

Divise et affecte

opOpAssign

 

=

%=

Affecte le reste de

opOpAssign

 

=

^^=

Élève à la puissance et affecte

opOpAssign

 

=

&=

Affecte le résultat de &

opOpAssign

 

=

|=

Affecte le résultat de |

opOpAssign

 

=

^=

Affecte le résultat de ^

opOpAssign

 

=

<<=

Affecte le résultat de <<

opOpAssign

 

=

>=

Affecte le résultat de >>

opOpAssign

 

=

>>=

Affecte le résultat de >>>

opOpAssign

 

=

~=

Affecte le résultat de ~

opOpAssign

 

=

opBinaryRight est utilisé lorsque l'objet peut apparaître du côté droit de l'opérateur. Supposons qu'un opérateur binaire que nous appellerons op apparaisse dans le programme :

 
Sélectionnez
x op y

Pour déterminer quelle fonction membre appeler, le compilateur considère les deux options suivantes :

 
Sélectionnez
// la définition pour x à gauche :
x.opBinary!"op"(y);
 
// la définition pour y à droite :
y.opBinaryRight!"op"(x);

Le compilateur choisit l'option qui correspond le mieux.
Dans la plupart des cas, il n'est pas nécessaire de définir opBinaryRight, sauf pour l'opérateur in : il est souvent plus pertinent de définir l'opérateur in au moyen de opBinaryRight.
Le nom de paramètre rhs qui apparaît dans les définitions ci-après est une abréviation de l'anglais right-hand side. Il indique que l'opérande apparaît du côté droit de l'opérateur :

 
Sélectionnez
x op y

Pour l'expression ci-dessus, le paramètre rhs représenterait la variable y.

56-1-3. Opérateur d'indexation et de tranchage d'éléments

Les opérateurs suivants permettent d'utiliser un type comme une collection d'éléments :

Description

Nom de fonction

Exemple d'utilisation

Accès à un élément

opIndex

collection[i]

Affectation d'un élément

opIndexAssign

collection[i] = 7

Opération unaire sur un élément

opIndexUnary

++collection[i]

Opération avec affectation à un élément

opIndexOpAssign

collection[i] *= 2

Nombre d'éléments

opDollar

collection[$-1]

Tranche de tous les éléments

opSlice

collection[]

Tranche de certains éléments

opSlice(size_t, size_t)

collection[i..j]

Nous aborderons ces opérateurs plus loin.

Les opérateurs suivants viennent d'une ancienne version de D. Leur utilisation est découragée :

Description

Nom de fonction

Exemple d'utilisation

Opération unaire sur tous les éléments

opSliceUnary (découragée)

++collection[]

Opération unaire sur certains éléments

opSliceUnary (découragée)

++collection[i..j]

Affectation de tous les éléments

opSliceAssign (découragée)

collection[] = 42

Affectation de certains éléments

opSliceAssign (découragée)

collection[i..j] = 7

Opération avec affectation de tous les éléments

opSliceOpAssign (découragée)

collection[] *= 2

Opération avec affectation de certains éléments

opSliceOpAssign (découragée)

collection[i..j] *= 2

56-1-4. Autres opérateurs

Les opérateurs suivants peuvent également être surchargés :

Description

Nom de fonction

Exemple d'utilisation

Appel de fonction

opCall

objet(42)

Conversion de type

opCast

to!int(objet

Transfert de fonction inexistante

opDispatch

objet.inexistante()

Ces opérateurs seront expliqués plus loin dans leur propre section.

56-1-5. Définir plusieurs opérateurs simultanément

Pour garder des exemples de codes courts, nous avons jusqu'à présent utilisé les opérateurs ++, +, et +=. Lorsqu'un opérateur est surchargé pour un type, il est concevable que de nombreux autres doivent l'être également. Par exemple, les opérateurs -, -- et -= sont aussi définis pour la Duree suivante :

 
Sélectionnez
struct Duree 
{
    int minute; 
 
    ref Duree opUnary(string op)()
        if (op == "++")
    {
        ++minute;
        return this;
    }
 
    ref Duree opUnary(string op)()
        if (op == "--")
    {
        --minute;
        return this;
    }
 
    ref Duree opOpAssign(string op)(in int montant)
        if (op == "+")
    {
        minute += montant;
        return this;
    }
 
    ref Duree opOpAssign(string op)(in int montant)
        if (op == "-")
    {
        minute -= montant;
        return this;
    }
}
 
unittest
{
    auto duree = Duree(10);
 
    ++duree;
    assert (duree.minute == 11);
 
    --duree;
    assert (duree.minute == 10);
 
    duree += 5;
    assert (duree.minute == 15);
 
    duree -= 3;
    assert (duree.minute == 12);
}
 
void main()
{}

Les surcharges d'opérateurs ci-dessus dupliquent beaucoup de code. Les seules différences entre les fonctions similaires sont surlignées. De telles duplications de code peuvent être réduites et quelquefois évitées au moyen des mixins de chaînes. Nous verrons également le mot-clé mixin dans un chapitre ultérieur. Je voudrais juste vous montrer brièvement comment ce mot-clé facilite la surcharge d'opérateurs.
mixin insère la chaîne spécifiée comme code source à l'endroit même où la déclaration mixin apparaît dans le code. La structure suivante est équivalente à celle plus avant :

 
Sélectionnez
struct Duree
{
    int minute;
 
    ref Duree opUnary(string op)()
        if ((op == "++") || (op == "--"))
    {
        mixin (op ~ "minute;");
        return this;
    }
 
    ref Duree opOpAssign(string op)(in int montant)
        if ((op == "+" || op == "-"))
    {
        mixin ("minute " ~ op ~ "= montant;");
        return this;
    }
}

Si les objets Duree doivent également pouvoir être multipliés ou divisés par un montant, la seule chose nécessaire est d'ajouter deux conditions supplémentaires à la contrainte de modèle :

 
Sélectionnez
struct Duree
{
    // ...
 
    ref Duree opOpAssign(string op)(in int montant)
        if ((op == "+") || (op == "-") ||
            (op == "*") || (op == "/"))
    {
        mixin ("minute " ~ op ~ "= montant;");
        return this;
    }
}
 
unittest
{
    auto duree = Duree(12);
 
    duree *= 4;
    assert (duree.minute == 48);
 
    duree /= 2;
    assert (duree.minute == 24);
}

En fait, les contraintes de modèles sont optionnelles :

 
Sélectionnez
ref Duree opOpAssign(string op)(in int montant)
    // ← pas de contrainte
{
    mixin ("minute " ~ op ~ "= montant;");
    return this;
}

56-1-6. Types de retour des opérateurs

Lorsqu'un opérateur est surchargé, il est conseillé de garder le même type de retour que le même opérateur sur les types fondamentaux. Cela aide à la compréhension du code et réduit la confusion.
Aucun opérateur ne renvoie void lorsqu'il est appliqué sur des types fondamentaux. Ceci devrait vous paraître évident pour certains opérateurs. Par exemple, le résultat de l'addition de deux valeurs int a + b est int.

 
Sélectionnez
int a = 1;
int b = 2;
int c = a + b; // c est initialisé avec la valeur de retour de
               // l'opérateur +

Les valeurs de retour de certains opérateurs sont moins évidentes. Par exemple, même les opérateurs comme ++i ont une valeur :

 
Sélectionnez
int i = 1;
writeln(++i); // affiche 2

L'opérateur ++ ne se contente pas d'incrémenter i, il renvoie également la nouvelle valeur de celui-ci. Plus exactement, ce qui est retour par ++ n'est pas seulement la nouvelle valeur de i mais plutôt la variable i elle-même. Nous pouvons le voir en affichant l'adresse du résultat de cette expression :

 
Sélectionnez
int i = 1;
writeln("L'adresse de i               : ", &i);
writeln("L'adresse du résultat de ++i : ", &(++i));

La sortie contient des adresses identiques :

 
Sélectionnez
L`adresse de i               : 7FFF39BFEE78
L`adresse du résultat de ++i : 7FFF39BFEE78

Je vous recommande de suivre les consignes suivantes lorsque vous surchargez des opérateurs pour vos propres types.

56-1-6-1. Les opérateurs qui modifient l'objet

À l'exception de opAssign, il est recommandé que les opérateurs qui modifient l'objet retournent ce même objet. Cette consigne a été observée ci-avant par MomentDeLaJournee.opOpAssign!"+" et Duree.opUnary!"++".
Pour retourner l'objet courant il faut suivre ces deux étapes :

  1. Le type de retour doit être le type de la structure, précédé du mot-clé ref qui signifie référence ;
  2. La fonction doit se terminer par return this, qui signifie retourner cet objet.

Les opérateurs qui modifient l'objet sont opUnary!"++", o pUnary!"--" et toutes les surcharges de opOpAssign.

56-1-6-2. Les opérateurs logiques

opEquals, qui représente == et != doit retourner un bool. Bien que l'opérateur in retourne normalement l'objet contenu, il peut tout aussi bien retourner un bool.

56-1-6-3. Les opérateurs d'ordre

opCmp, qui représente <, <=, > et >= doit retourner un int.

56-1-6-4. 1.6.4 Les opérateurs qui créent un nouvel objet

Certains opérateurs doivent créer et retourner un nouvel objet :

  • les opérateurs unaires -, +,  et l'opérateur binaire ;
  • les opérateurs arithmétiques +, -, *, /, % et ^^ ;
  • les opérateurs bit à bit &, |, ^, <<, >>, et >>> ;
  • comme vu dans le chapitre précédent, opAssign retourne une copie de l'objet au moyen de return this.

À des fins d'optimisation, il peut être préférable que opAssign renvoie une const ref pour les grosses structures. Je n'utiliserai pas cette optimisation dans ce livre.

Comme exemple d'un opérateur qui crée un nouvel objet, définissons la surcharge de l'opérateur opBinary!"+" pour Duree. Cet opérateur doit additionner deux objets Duree et en retourner un nouveau.

 
Sélectionnez
struct Duree {
    int minute;
 
    Duree opBinary(string op)(in Duree, rhs) const
            if (op == "+") {
        return Duree(minute + rhs.minute); // nouvel objet
    }
}

Cette définition nous permet d'additionner deux objets Duree au moyen de l'opérateur + :

 
Sélectionnez
auto dureeVoyage = Duree(10);
auto dureeRetour = Duree(11);
Duree dureeTotale;
// ...
dureeTotale = dureeVoyage + dureeRetour;

Le compilateur remplace cette expression par l'appel de fonction membre suivant sur l'objet dureeVoyage :

 
Sélectionnez
// Équivalent à l'expression ci-avant
dureeTotale = dureeVoyage.opBinary!"+"(dureeRetour);
56-1-6-5. opDollar

Comme il retourne le nombre d'éléments d'un conteneur, le type le plus adapté pour opDollar est size_t. Néanmoins le type de retour peut aussi être différent (par exemple, int).

56-1-6-6. Les opérateurs non contraints

Pour certains opérateurs, le type de retour dépend entièrement de la conception du type défini par l'utilisateur : le * unaire, opCall, opCast, opDispatch, opSlice et toutes les variétés de opIndex.

56-1-7. Les comparaisons d'égalité avec opEquals()

Cette fonction membre définit les comportements des opérateurs == et !=. Le type de retour de opEquals est bool.
Pour les structures, le paramètre de opEquals peut être défini comme in. Cependant, pour des questions de vitesse opEquals peut être défini comme un modèle qui prend une auto ref const (notez également les parenthèses vides ci-après) :

 
Sélectionnez
bool opEquals()(auto ref const MomentDeLaJournee rhs) const {
    // ...
}

Comme nous l'avons vu dans le chapitre sur les lvalues et les rvalues, auto ref permet de passer les lvalues par référence et les rvalues par copie. Néanmoins, puisque les rvalues ne sont pas copiées, mais déplacées, la signature ci-avant est efficace pour les lvalues comme pour les rvalues.
Pour éviter toute confusion, opEquals et opCmp doivent travailler de façon cohérente. Pour deux objets avec lesquels opEquals retourne true, opCmp doit retourner zéro.
Une fois qu'opEquals a été défini pour l'égalité, le compilateur utilise son opposé pour l'inégalité :

 
Sélectionnez
x == y;
// l'équivalent à l'expression précédente :
x.opEquals(y);
 
x != y;
// l'équivalent à l'expression précédente :
!(x.opEquals(y));

Normalement, il n'est pas nécessaire de définir opEquals() pour les structures. Le compilateur la génère automatiquement. La fonction opEquals() générée automatiquement compare tous les membres individuellement.
Parfois, ce comportement automatique de l'égalité de deux objets doit être redéfini. Par exemple, certains membres peuvent ne pas être significatifs pour cette comparaison, ou alors l'égalité peut dépendre d'une logique plus complexe.
Juste pour exemple, définissons opEquals() de façon à ne pas tenir compte des minutes :

 
Sélectionnez
struct MomentDeLaJournee
{
    int heure;
    int minute;
 
    bool opEquals(in MomentDeLaJournee rhs) const
    {
        return heure == rhs.heure;
    }
}
 
// ....
 
assert (MomentDeLaJournee(20, 10) == MomentDeLaJournee(20, 59));

Puisque la comparaison d'égalité ne considère plus que le membre heure, 20h10 et 20h59 reviennent à la même chose. (Ceci vous est juste montré à titre d'exemple, il est clair que ce genre de comparaison d'égalité génère de la confusion.)

56-1-8. L'ordre avec opCmp()

Les opérateurs d'ordre déterminent l'ordre de tri des objets. Tous les opérateurs d'ordre <, <=, > et >= sont couverts par la fonction opCmp().
Pour les structures, le paramètre de opCmp peut être défini comme in. Néanmoins, comme pour opEquals, il est plus efficace de définir opCmp comme un modèle qui prend un paramètre auto ref const :

 
Sélectionnez
int opCmp()(auto ref const MomentDeLaJournee rhs) const {
    // ...
}

Pour éviter toute confusion, opEquals et opCmp doivent travailler de façon cohérente. Pour deux objets avec lesquels opEquals retourne true, opCmp doit retourner zéro.
Imaginons qu'un de ces quatre opérateurs soit utilisé dans le code suivant :

 
Sélectionnez
if (x op y) { // ← op est soit <, <=, > ou >=

Le compilateur transforme cette expression en l'expression logique suivante et utilise le résultat dans la nouvelle expression logique :

 
Sélectionnez
if (x.opCmp(y) op 0 {

Prenons l'opérateur <=, par exemple :

 
Sélectionnez
if (x <= y) {

Le compilateur génère le code suivant en coulisse :

 
Sélectionnez
if (x.opCmp(y) <= 0) {

Pour qu'une fonction opCmp définie par l'utilisateur fonctionne correctement, cette fonction doit renvoyer un résultat selon les règles suivantes :

  • une valeur négative si l'objet de gauche est considéré comme précédent l'objet de droite ;
  • une valeur positive si l'objet de gauche est considéré comme suivant l'objet de droite ;
  • zéro si les deux objets ont le même ordre de tri.

Pour pouvoir prendre en charge ces valeurs, le type de retour de opCmp() doit être un int, pas un bool.
Voici une façon d'ordonner les objets MomentDeLaJournee en comparant tout d'abord les membres heure, puis les membres minute (seulement si les valeurs des membres heure sont égales) :

 
Sélectionnez
int opCmp(in MomentDeLaJournee rhs) const {
    /* Note: la soustraction est un bogue ici si les valeurs peuvent
     * déborder. (voir l'avertissement suivant dans le texte */
    
    return (heure == rhs.heure
        ? minute - rhs.minute
        : heure - rhs.heure);
}

Cette définition retourne la différence entre les valeurs de minute quand les membres heure sont identiques, et la différence entre les membres heure autrement. La valeur retournée sera négative lorsque l'objet de gauche arrive avant dans l'ordre chronologique, positive lorsque l'objet de droite arrive avant, et zéro si les deux objets représentent le même moment de la journée.
Avertissement : utiliser des soustractions pour implémenter opCmp est un bogue si les valeurs valides d'un membre peuvent causer des débordements. Par exemple, les deux objets ci-après seront triés incorrectement puisque l'objet avec la valeur -2 est calculé comme étant plus grand que celui avec la valeur int.max.

 
Sélectionnez
struct S {
    int i;
 
    int opCmp(in S rhs) const {
        return i - rhs.i;         // ← bogue
    }
}
 
void main() {
    assert (S(-2) > S(int.max)); // ← mauvais ordre de tri
}

D'un autre côté, la soustraction est parfaitement acceptable pour les objets MomentDeLaJournee car aucune valeur valide des membres de cette structure ne peut causer de débordement lors de la soustraction.
Pour comparer des tranches (ainsi que tous les types de chaînes et les intervalles), vous pouvez utiliser std.algorithm.cmp. cmp() compare les tranches de manière lexicographique et produit une valeur négative, nulle ou positive en fonction de leur ordre. Le résultat peut être directement utilisé comme valeur de retour de opCmp :

 
Sélectionnez
import std.algorithm;
 
struct S {
    string nom;
 
    int opCmp(in S rhs) const
    {
        return cmp(nom, rhs.nom);
    }
}

Une fois qu'opCmp est défini, un type peut être utilisé avec des algorithmes de tri comme std.algorithm.sort. Lorsque sort() travaille sur les éléments, l'opérateur opCmp() est appelé en arrière-plan pour déterminer leur ordre. Le programme suivant construit dix objets avec des valeurs aléatoires et les trie avec sort() :

 
Sélectionnez
import std.random;
import std.stdio;
import std.string;
import std.algorithm;
 
struct MomentDeLaJournee
{
    int heure;
    int minute;
    int opCmp(in MomentDeLaJournee rhs) const {
        return (heure == rhs.heure
            ? minute - rhs.minute
            : heure - rhs.heure);
    }
 
    string toString() const {
        return format("%02s:%02s", heure, minute);
    }
}
 
void main() {
    MomentDeLaJournee[] moments;
 
    foreach (i; 0 .. 10) {
        moments ~= MomentDeLaJournee(uniform(0, 24), uniform(0, 60));
    }
 
    sort(moments);
 
    writeln(moments);
}

Comme attendu, les éléments sont triés du plus tôt au plus tard :

 
Sélectionnez
[03:40, 04:10, 09:06, 10:03, 10:09, 11:04, 13:42, 16:40, 18:03, 21:08]

56-1-9. Appeler les objets comme des fonctions : opCall()

Les parenthèses autour de la liste de paramètres lors de l'appel de fonction sont aussi un opérateur. Nous avons déjà vu comment static opCall() rend possible l'utilisation d'un nom de type comme fonction. static opCall() permet de créer des objets de types définis par l'utilisateur avec des valeurs par défaut au moment de l'exécution.
Un opCall() non statique permet d'utiliser les objets de type définis par l'utilisateur comme des fonctions :

 
Sélectionnez
Foo foo;
foo();

L'objet foo ci-dessus est appelé comme une fonction.
Par exemple, considérons une struct qui représente une équation linéaire. Cette struct sera utilisée pour calculer les valeurs de y de l'équation linéaire suivante pour les valeurs x spécifiques :

 
Sélectionnez
y = ax + b

La méthode opCall() suivante calcule et retourne simplement la valeur de y selon cette équation :

 
Sélectionnez
struct EquationLineaire {
    double a;
    double b;
 
    double opCall(double x) const {
        return a * x + b;
    }
}

Avec cette définition, chaque objet de EquationLineaire représente une équation linéaire pour des valeurs spécifiques a et b. Un tel objet peut être utilisé comme une fonction qui calcule des valeurs de y :

 
Sélectionnez
EquationLineaire equation = { 1.2, 3.4 };
// l'objet est utilisé comme une fonction
double y = equation(5.6);

Note : définir opCall pour une structure désactive le constructeur par défaut généré par le compilateur. C'est pourquoi la syntaxe { } est utilisée ci-dessus au lieu de la version recommandée en EquationLineaire(1.3, 3.4) . Lorsque cette dernière syntaxe est désirée, une méthode static opCall() qui prend DEUX paramètres double doit également être définie.

La variable equation ci-avant représente l'équation y = 1,2x + 3,4. L'utilisation de cet objet comme une fonction exécute la fonction membre opCall().
Cette fonctionnalité peut-être utile pour définir et stocker les valeurs de a et b une fois dans un objet et utiliser cet objet plusieurs fois par la suite. Le code suivant utilise un tel objet dans une boucle :

 
Sélectionnez
EquationLineaire equation = { 0.01, 0.4 };
 
for (double x = 0.0; x <= 1.0; x += 0.125) {
    writefln("%f: %f", x, equation(x));
}

Cet objet représente l'équation y = 0,01x + 0,4. Il est utilisé pour calculer les résultat des valeurs de x dans l'intervalle 0 à 1.

56-1-10. Les opérateurs d'indexation

opIndex, opIndexAssign, opIndexUnary, opIndexOpAssign et opDollar rendent l'utilisation des opérateurs d'indexation sur les types définis par l'utilisateur similaires à des tableaux comme dans des objets.
Contrairement aux tableaux, ces opérateurs prennent aussi en charge l'indexation sur plusieurs dimensions. Les multiples valeurs d'index sont spécifiées comme une liste séparée par des virgules à l'intérieur des crochets (ex. : objetindex1). Dans les exemples suivants, nous utiliserons ces opérateurs avec une seule dimension et nous aborderons leurs usages multidimensionnels dans le chapitre sur plus de modèles.
La variable deque dans les exemples suivants est un objet de struct FileADoubleSens, que nous définirons plus tard ; et e est une variable de type int.
opIndex sert à l'accès aux éléments. L'index passé entre les crochets devient le paramètre de la fonction opérateur :

 
Sélectionnez
e = deque[3];               // l'élement à l'index 3
e = deque.opIndex(3);       // équivalent à la ligne ci-dessus

opIndexAssign sert à affecter une valeur à un élément. Le premier paramètre est la valeur qui est affectée et le second est l'index de l'élément :

 
Sélectionnez
deque[5] = 55;              // Affecte 55 à l'élément à l'index 5
deque.opIndexAssign(55, 5); // équivalent à la ligne ci-dessus

opIndexUnary est similaire à opUnary. La différence est que l'opération est appliquée à l'élément à l'index spécifié :

 
Sélectionnez
++deque[4];                 // Incrémente l'élément à l'index 4
deque.opIndexUnary!"++"(4); // équivalent à la ligne ci-dessus

opIndexOpAssign est similaire à opOpAssign. La différence est que l'opération est appliquée à un élément :

 
Sélectionnez
deque[6] += 66;                   // ajoute 66 à l'élément à l'index 6
deque.opIndexOpAssign!"+"(66, 6); // équivalent à la ligne ci-dessus

opDollar définit le caractère $ qui est utilisé durant les indexations et les tranchages. Il sert à retourner le nombre d'éléments dans le conteneur :

 
Sélectionnez
e = deque[$ - 1];                // le dernier élément
e = deque[deque.opDollar() - 1]; // équivalent à la ligne ci-dessus
56-1-10-1. Un exemple d'opérateurs d'indexation

Une file à double sens (Double Ended Queue, aussi appelée Deque) est une structure de données qui est similaire aux tableaux, mais qui fournit également une insertion en début de séquence efficace (au contraire, insérer un élément au début d'un tableau nécessite de déplacer les éléments existants dans un tableau nouvellement créé).
Une manière d'implémenter une file à double sens est d'utiliser deux tableaux en coulisse, mais d'utiliser le premier en sens inverse. L'élément qui est inséré en tête de séquence est en fait ajouté à la fin du tableau de tête. Finalement cette opération est aussi efficace qu'ajouter un élément en fin de séquence.
La struct suivante implémente une file à double sens qui surcharge les opérateurs que nous avons vus dans cette section :

 
Sélectionnez
import std.stdio;
import std.string;
import std.conv;
 
struct FileADoubleSens // Aussi appelée Deque
{
private:
 
    /* Les éléments sont représentés comme le chaînage des deux tranches 
     * membres. Néanmoins, la tête est indexée en sens inverse de 
     * manière à ce que le premier élément de la collection entière soit
     * tete[$-1], le second tete[$-2], etc.:
     *
     * tete[$-1], tete[$-2], tete[$-3],...tete[0], queue[0], ... queue[$-1]
     */
    int[] tete;
    int[] queue;
 
    /* Détermine la tranche réelle où l'élément spécifié reside
     * et le retourne en référence */
    ref inout(int) elementA(size_t index) inout {
        return (index < tete.length
                ? tete[$-1 - index]
                : queue[index - tete.length]);
    }
 
public:
 
    string toString() const {
        string resultat;
 
        foreach_reverse (element; tete) {
            resultat ~= format("%s ", to!string(element));
        }
 
        foreach (element; queue) {
            resultat ~= format("%s ", to!string(element));
        }
 
        return resultat;
    }
 
    /* Note: Comme nous le verrons dans le chapitre suivant, 
       le code suivant est une implémentation plus simple et plus 
       efficace de toString(): */
    version (none) {
        void toString(void delegate(const(char)[]) sink) const {
            import std.format;
            import std.range;
 
            formattedWrite(sink, "%(%s %)", chain(head.retro, tail));
        }
    }
 
    /* ajoute un élément en tête de collection. */
    void insererEnTete(int valeur) {
        tete ~= valeur;
    }
 
    /* ajoute un élément en bout de collection. */
    ref FileADoubleSens opOpAssign(string op)(int valeur)
            if (op == "~") {
        queue ~= valeur;
        return this;
    }
 
    /* Retourne l'élément spécifié
     *
     * Exemple: deque[index]
     */
    inout(int) opIndex(size_t index) inout {
        return elementA(index);
    }
 
    /* applique une opération unaire à l'élément spécifié.
     *
     * exemple: ++deque[index]
     */
    int opIndexUnary(string op)(size_t index) {
        mixin ("return " ~ op ~ " elementA(index);");
    }
 
    /* affecte une valeur à l'élément spécifié.
     * 
     * exemple: deque[index] = valeur
     */
    int opIndexAssign(int valeur, size_t index) {
        return elementA(index) = valeur;
    }
 
    /* utilise l'élément spécifié et une valeur dans une opération
     * et affecte ce résultat à cet élément.
     *
     * exemple: deque[index] += valeur
     */
    int opIndexOpAssign(string op)(int valeur, size_t index) {
        mixin ("return elementA(index) " ~ op ~ "= valeur;");
    }
 
    /* définit le caractère $, qui est la taille de la collection.
     * 
     * exemple: deque[$ - 1]
     */
    size_t opDollar() const {
        return tete.length + queue.length;
    }
}
 
void main() {
    auto deque= FileADoubleSens();
    
    foreach (i; 0 .. 10) {
        if (i % 2) { 
            deque.insererEnTete(i);
        } else {
            deque ~= i;
        }
    }
 
    writefln("Élément à l'index 3: %s",
             deque[3]);   // Accès à l'élément
    ++deque[4];           // Incrément d'un élément
    deque[5] = 55;        // Affectation d'un élément
    deque[6] += 66;       // Addition à un élément
 
    (deque ~= 100) ~= 200);
 
    writeln(deque);
}

Selon les lignes directrices ci-avant, le type de retour de opOpAssign est ref pour que l'opérateur = puisse être chaîné pour la même collection :

 
Sélectionnez
(deque ~= 100) ~= 200;

Finalement, 100 est 200 sont ajoutés à la même collection :

 
Sélectionnez
Élément à l'index 3: 3
9 7 5 3 2 55 68 4 6 8 100 200

56-1-11. Les opérateurs de tranchage

opSlice permettent de trancher les types définis par l'utilisateur au moyen de l'opérateur.
En plus de cet opérateur, il y a également opSliceUnary, opSliceAssign et opSliceOpAssign, mais leur utilisation n'est pas recommandée.
D prend en charge les tranches multidimensionnelles. Nous verrons un exemple multidimensionnel plus tard dans le chapitre additionnel sur les modèles. Bien que les méthodes décrites dans ce chapitre puissent également être employées avec une seule dimension, elles ne correspondent pas aux opérateurs d'indexation qui ont été définis plus haut et mettent en œuvre les modèles, qui n'ont pas encore été abordés. C'est pourquoi nous verrons une utilisation non templatée de opSlice dans ce chapitre ; qui marche avec une seule dimension (utiliser opSlice de cette manière n'est pas non plus recommandé).
opSlice prend deux formes différentes :

  • les crochets peuvent être vides comme dans deque pour signifier tous les éléments ;
  • les crochets peuvent contenir un intervalle numérique comme dans deque.fin pour signifier les éléments dans un intervalle spécifié.

Les opérateurs de tranchage sont relativement plus complexes que les autres opérateurs parce qu'ils mettent en œuvre deux concepts distincts : les conteneurs et les intervalles. Nous verrons ces concepts plus en détail dans les chapitres suivants.
Dans le cas du tranchage unidimensionnel qui n'utilise pas les modèles, opSlice retourne un objet qui représente un intervalle d'éléments d'un conteneur. Cet objet a la responsabilité de définir les opérations qui sont appliquées sur les éléments de cet intervalle. Par exemple, en coulisse, l'expression suivante est exécutée en appelant opSlice pour obtenir un objet intervalle puis en appliquant opOpAssign!"*" sur cet objet :

 
Sélectionnez
deque[] *= 10;    // multiplie tous les éléments par 10
 
// équivalent au code ci-dessus
{
    auto intervalle = deque.opSlice();
    range.opOpAssign!"*"(10);
}

En conséquence, les opérateurs opSlice de FileADoubleSens retournent un objet spécial Intervalle sur lequel on peut appliquer ces opérations :

 
Sélectionnez
import std.exception;
 
struct FileADoubleSens {
    // ...
 
    /* Retourne un intervalle qui représente tous les éléments.
     * (La structure 'Intervalle' est définie plus loin).
     *
     * ex : deque[] 
     */
    inout(Intervalle) opSlice() inout {
        return inout(Intervalle)(tete[], queue[]);
    }
 
    /* Retourne un intervalle qui représente certains des éléments
     * 
     * ex : deque[debut..fin]
     */
    inout(Intervalle) opSlice(size_t debut, size_t fin) inout {
        enforce(fin <= opDollar());
        enforce(debut <= fin);
 
        /* Détermine quelles parties de 'tete' et 'queue' 
           correspondent à l'intervalle spécifié: */
        if (debut < tete.length) {
            if (fin < tete.length) {
                /* l'intervale entier est dans 'tete'. */
                return inout(Intervalle)(tete[$ - fin .. $ - debut],
                                         []);
            } else {
                /* Une partie de l'intervalle est dans 'tete' et 
                   le reste est dans 'queue'. */
                return inout(Intervalle)(tete[0 .. $ - debut],
                                         queue[0 .. fin - tete.length]);
            }
        } else {
            /* L'intervalle est complètement dans 'queue'. */
            return inout(Intervalle)(
                            [],
                            queue[debut-tete.length .. fin - tete.length]);
        }
    }
 
    /* Représente un intervalle d'éléments de la collection. 
       Cette structure a la responsabilité de définir les opérateurs
       opUnary, opAssign et opOpAssign. */
    struct Intervalle {
        int[] intervalleTete;
        int[] intervalleQueue;
 
        /* Applique les opérations unaires aux éléments de l'intervalle. */
        Intervalle opUnary(string op)() {
            mixin(op ~ "intervalleTete[];");
            mixin(op ~ "intervalleQueue[];");
            return this;
        }
 
        /* Affecte une valeur spécifiée à chaque élément de l'intervalle */
        Intervalle opAssign(int valeur) {
            intervalleTete[] = valeur;
            intervalleQueue[] = valeur;
            return this;
        }
 
        /* Utilise chaque élément et une valeur dans une opération binaire
           et réaffecte le résultat à cet élément. */
        Intervalle opOpAssign(string op)(int valeur)
        {
            mixin("intervalleTete[] " ~ op ~ "= valeur;");
            mixin("intervalleQueue[] " ~ op ~ "= valeur;");
            return this;
        }
    }
}
 
void main() {
    auto deque = FileADoubleSens();
 
    foreach (i; 0 .. 10) {
        if (i % 2) {
            deque.insererEnTete(i);
        } else {
            deque ~= i;
        }
    }
 
    writeln(deque);
    deque[] *= 10;
    deque[3 .. 7] = -1;
    writeln(deque);
}

Donne comme résultat :

 
Sélectionnez
9 7 5 3 1 0 2 4 6 8 
90 70 50 -1 -1 -1 -1 40 60 80

56-1-12. opCast pour les conversions de types

opCast définit les conversions de types explicites. Il peut être surchargé séparément pour chaque type cible. Comme nous l'avons vu dans les chapitres antérieurs, les conversions de types explicites sont effectuées par la fonction to et l'opérateur cast.
opCast est aussi un modèle, mais a un format différent : le type cible est spécifié au moyen de la syntaxe (T : type_cible) :

 
Sélectionnez
type_cible opCast(T: type_cible)() {
    // ...
}

Cette syntaxe deviendra claire lorsque nous aurons abordé le chapitre sur les modèles.
Modifions la définition de Duree pour qu'elle ait désormais deux membres : heures et minutes. L'opérateur qui convertit les objets de ce type en double peut être défini ainsi :

 
Sélectionnez
import std.stdio;
import std.conv;
 
struct Duree {
    int heure;
    int minute;
 
    double opCast(T: double)() const {
        return heure + (to!double(minute) / 60);
    }
}
 
void main() {
    auto duree = Duree(2, 30);
    double d = to!double(duree);
    // (pourrait aussi être 'cast(double)duree'
 
    writeln(d);
}

Le compilateur remplace la conversion de type par l'appel suivant :

 
Sélectionnez
double d = duree.opCast!double();

La conversion vers double ci-dessus produit 2,5 pour deux heures et trente minutes :

 
Sélectionnez
30/12/99

56-1-13. L'opérateur joker opDispatch

opDispatch est appelé à chaque fois qu'on essaie d'accéder à un membre absent d'un objet. Toutes les tentatives d'accès à des membres absents sont redirigées vers cette fonction.
Le nom du membre absent devient la valeur de paramètre modèle de opDispatch.
Le code suivant montre une définition simple :

 
Sélectionnez
import std.stdio;
 
struct Foo {
    void opDispatch(string nom, T)(T parametre) {
        writefln("Foo.opDispatch - nom: %s, valeur: %s",
                 nom, parametre);
    }
}
 
void main() {
    Foo foo;
    foo.uneFonctionInexistante(42);
    foo.uneAutreFonctionInexistante(100);
}

Les appels à des membres inexistants ne produisent pas d'erreur de compilation. À la place, tous ces appels sont envoyés à opDispatch. Le premier paramètre du modèle est le nom du membre. Les valeurs des paramètres utilisés lors de l'appel apparaissent aussi comme paramètres de opDispatch :

 
Sélectionnez
Foo.opDispatch - nom: uneFonctionInexistante, valeur: 42
Foo.opDispatch - nom: uneAutreFonctionInexistante, valeur: 100

Le paramètre nom peut être utilisé dans la fonction afin de décider comment l'appel à la fonction inexistante doit être géré :

 
Sélectionnez
switch (nom) {
    // ...
}

56-1-14. Recherche d'inclusion avec opBinaryRight!« in »

Cet opérateur permet de définir le comportement de l'opérateur in pour les types définis par l'utilisateur. Il est couramment utilisé avec les tableaux associatifs afin de déterminer si une valeur existe dans le tableau pour une clé spécifiée.
À la différence des autres opérateurs, cet opérateur est normalement surchargé pour le cas où l'objet apparaît du côté droit :

 
Sélectionnez
if (moment in pauseDejeuner) {

Le compilateur utilise opBinaryRight en coulisse :

 
Sélectionnez
// l'équivalent de code ci-dessus
if (pauseDejeuner.opBinaryRight!"in"(moment)) {

Il existe aussi l'opérateur !in qui détermine si une clé spécifiée n'existe pas dans le tableau :

 
Sélectionnez
if (a !in b) {

!in ne peut pas être surchargé parce que le compilateur utilise à la place le négatif du résultat de l'opérateur in :

 
Sélectionnez
if (!(a in b)) { // équivalent au code ci-dessus
56-1-14-1. Exemple d'opérateur in

Le programme suivant définit un type LapsDeTemps en plus de Duree et MomentDeLaJournee. L'opérateur in qui est défini pour LapsDeTemps détermine si un moment est compris dans ce laps de temps.
Afin de garder le code concis, le programme suivant ne définit que les fonctions membres nécessaires.
Notez comment MomentDeLaJournee est utilisé harmonieusement dans la boucle for. Cette boucle montre l'utilité de la surcharge des opérateurs.

 
Sélectionnez
import std.stdio;
import std.string;
 
struct Duree {
    int minute;
}
 
struct MomentDeLaJournee {
    int heure;
    int minute;
 
    ref MomentDeLaJournee opOpAssign(string op)(in Duree duree)
            if (op == "+") {
        minute += duree.minute;
 
        heure += minute / 60;
        minute %= 60;
        heure %=24;
 
        return this;
    }
 
    int opCmp(in MomentDeLaJournee rhs) const {
        return (heure == rhs.heure
                    ? minute - rhs.minute
                    : heure - rhs.heure);
    }
 
    string toString() const {
        return format("%02s:%02s", heure, minute);
    }
}
 
struct LapsDeTemps {
    MomentDeLaJournee debut;
    MomentDeLaJournee fin;  // non inclusif
 
    bool opBinaryRight(string op)(MomentDeLaJournee moment) const 
            if (op == "in") {
        return (moment >= debut) && (moment < fin);
    }
}
 
void main() {
    auto pauseDejeuner = LapsDeTemps(MomentDeLaJournee(12, 00),
                                     MomentDeLaJournee(13, 00));
 
    for (auto moment = MomentDeLaJournee(11, 30);
         moment < MomentDeLaJournee(13, 30);
         moment += Duree(15)) {
        
        if (moment in pauseDejeuner) {
            writeln(moment, " est pendant la pause déjeuner");
        } else {
            writeln(moment, " est en dehors de la pause déjeuner");
        }
    }
}

En sortie :

 
Sélectionnez
11:30 est en dehors de la pause déjeuner
11:45 est en dehors de la pause déjeuner
12:00 est pendant la pause déjeuner
12:15 est pendant la pause déjeuner
12:30 est pendant la pause déjeuner
13:00 est en dehors de la pause déjeuner
13:15 est en dehors de la pause déjeuner

56-1-15. Exercice

Définissez un type fraction qui stocke son numérateur et son dénominateur comme des membres de type long. Un tel type peut être utile parce qu'il ne perd pas de valeur comme float, double et real à cause de leur précision. Par exemple, multiplier une valeur ``double` de 1,0 / 3 par 3 ne donne pas 1,0 alors que multiplier un objet Fraction qui représente la fraction 1/3 donnerait exactement 1 :

 
Sélectionnez
struct Fraction {
    long num;
    long den;
 
    /* Comme commodité, le constructeur utilise la valeur par 
       défaut 1 pour le dénominateur. */
    this(long num, long den = 1) {
        enforce(den != 0, "Le dénominateur ne peut être zéro");
 
        this.num = num;
        this.den = den;
 
        /* S'assurer que le dénominateur est toujours positif 
           simplifiera les définitions de quelques opérateurs */
        if (this.den < 0) {
            this.num = -this.num;
            this.den = -this.den;
        }
    }
 
    /* ... À vous de définir les surcharges d'opérateurs... */
}

Définissez les opérateurs demandés afin de rendre ce type commode et aussi proche à utiliser que les types fondamentaux que possible. Assurez-vous que la définition de ce type passe tous les tests unitaires suivants. Ces tests unitaires vérifient le comportement suivant :

  • une exception doit être lancée lorsque l'on construit un objet avec un dénominateur à zéro (c'est déjà pris en compte par l'expression enforce ci-avant) ;
  • produire l'opposé de la valeur. Par exemple, l'opposé de 1/3 devrait être -1/3 et l'opposé de -2/5 devrait être 2/5.
  • Incrémenter et décrémenter la valeur avec ++ et .

    • Supporter les quatre opérations arithmétiques : pouvoir modifier la valeur d'un objet au moyen de +=, -=, *= et /= ; et le résultat de l'utilisation de deux objets avec les opérateurs +, -, * et /. Comme avec le constructeur, la division par zéro devrait être empêchée. Pour rappel, voici les formules des opérations arithmétiques qui mettent en œuvre deux fractions a/b et c/d :

      • addition : a/b + c/d = (a×d + c×b) / (b×d)
      • soustraction : a/b − c/d = (a×d − c×b) / (b×d)
      • multiplication : a/b × c/d = (a×c) / (b×d)
      • division : (a/b) / (c/d) = (a×d) / (b×c)
    • La valeur concrète (et nécessairement imprécise) de l'objet peut être convertie en double.
    • L'ordre de tri et le opération de comparaison doivent se baser sur les valeurs concrètes des fractions et pas sur les valeurs des numérateurs et dénominateurs. Par exemple, les fractions 1/3 et 20/60 doivent être considérées comme égales.
 
Sélectionnez
unittest {
    /* Doit lancer une exception quand le dénominateur est zéro */
    assertThrown(Fraction(42, 0));
 
    /* Commençons avec 1/3 */
    auto a = Fraction(1, 3);
 
    /* -1/3 */
    assert (-a == Fraction(-1, 3));
 
    /* 1/3 + 1 == 4/3 */
    ++a;
    assert (a == Fraction(4, 3));
 
    /* 4/3 - 1 == 1/3 */
 
Sélectionnez
a;
assert (a == Fraction(1, 3));
 
/* 1/3 + 2/3 == 3/3 */
a += Fraction(2, 3);
assert (a == Fraction(1));
 
/* 3/3 - 2/3 == 1/3 */
a -= Fraction(2, 3);
assert (a == Fraction(1, 3));
 
/* 1/3 * 8 == 8/3 */
a *= 8;
assert (a == Fraction(8, 3));
 
/* 8/3 / 16/9 == 3/2 */
a /= Fraction(16, 9);
assert (a == Fraction(3, 2));
 
/* Doit produire l'équivalent en type double
 
    Notez que si double ne pas pas représenter toutes les valeurs,
 
    précisément, 1,5 est une exception. C'est pourquoi ce test est
 
    effectué à ce stade. */
 
assert (to!double(a) == 1.5);
 
/* 1,5 + 2, 5 == 4 */
assert (a + Fraction(5, 2) == Fraction(4, 1));
 
/* 1,5 − 0,75 == 0,75 */
assert (a - Fraction(3, 4) == Fraction(3, 4));
 
/* 1,5 × 10 == 15 */
assert (a * 10 == Fraction(15, 1));
 
/* 1,5 / 4 == 3 / 8 */
assert (a / Fraction(4) == Fraction(3, 8));
 
/* Doit lancer une exception lors d'une division par zéro */
assertThrown(Fraction(42, 1) / Fraction(0));
 
/* Celui avec le petit numérateur est avant */
assert (Fraction(3, 5) < Fraction(4, 5));
 
/* Celui avec le plus grand dénominateur est avant */
assert (Fraction(3, 9) < Fraction(3, 8));
assert (Fraction(1, 1_000) > Fraction(1, 10_000));
 
/* Celui avec la plus petite valeur est avant */
assert (Fraction(10, 100) < Fraction(1, 2));
 
/* Les négatifs sont avant */
assert (Fraction(-1, 2) < Fraction(0));
assert (Fraction(1, -2) < Fraction(0));
 
/* Les valeurs égales doivent être à la fois <= et >= */
assert (Fraction(-1, -2) <= Fraction(1, 2));
assert (Fraction(1, 2) <= Fraction(-1, -2));
assert (Fraction(3, 7) <= Fraction(9, 21));
assert (Fraction(3, 7) >= Fraction(9, 21));
 
/* ceux qui ont une valeur égale doivent être égaux */
assert (Fraction(1, 3) == Fraction(20, 60));
 
/* Ceux qui ont une valeur égale avec le signe doivent être égaux */
assert (Fraction(-1, 2) == Fraction(1, -2));
assert (Fraction(1, 2) == Fraction(-1, -2));

56-2. La solution

L'implémentation qui suit passe tous les tests unitaires avec succès. Les choix de conception ont été inclus dans le code en commentaires.
Certaines des fonctions de cette structure peuvent être implémentées de manière plus performante. Par ailleurs, il serait bénéfique de normaliser le numérateur et le dénominateur. Par exemple, au lieu de garder les valeurs 20 et 60, celles-ci pourraient être divisées par leur plus grand commun diviseur et être remplacées respectivement par 1 et 3. Autrement, le numérateur et le dénominateur augmenteraient à la plupart des opérations.

 
Sélectionnez
import std.exception;
import std.conv;
 
struct Fraction {
 
    long num; // numérateur
    long den; // dénominateur
    /* Par commodité, le constructeur utilise une valeur de 1 pour le 
       dénominateur. */
    this(long num, long den = 1) {
        enforce(den != 0, "Le dénominateur ne peut pas être 0");
 
        this.num = num;
        this.den = den;
 
        /* S'assurer que le dénominateur est toujours positif 
           simplifiera les définitions de certains opérateurs. */
        if (this.den < 0) {
            this.num = -this.num;
            this.den = -this.den;
        }
    }
 
    /* - unaire: retourne l'opposé de cette fraction */
    Fraction opUnary(string op)() const
            if (op == "-") {
        /* On construit juste un objet anonyme et on le retourne */
        return Fraction(-num, den);
    }
 
    /* ++: incrémente la valeur de la fraction par un. */
    ref Fraction opUnary(string op)()
            if (op == "++") {
        /* Nous aurions pu utiliser 'this += Fraction(1)' ici. */
        num += den;
        return this;
    }
 
    ref Fraction opUnary(string op)()
            if (op == "--") {
        /* Nous aurions pu utiliser 'this -= Fraction(1)' ici. */
        num -= den;
        return this;
    }
 
    /* +=: Ajoute la Fraction côté droit à celle-ci */
    ref Fraction opOpAssign(string op)(in Fraction rhs)
            if (op == "+") {
        /* Formule de l'addition:a/b + c/d = (a*d + c*b) / (b*d) */
        num = (num * rhs.den) + (rhs.num * den);
        den *= rhs.den;
        return this;
    }
 
    /* -=: Soustrait la Fraction côté droit à celle-ci */
    ref Fraction opOpAssign(string op)(in Fraction rhs)
            if (op == "-") {
        /* Nous utilisons les opérateurs += et le - unaire 
           que nous avons déjà définis. Autrement, nous 
           aurions pu utiliser la formule de la soustraction,
           de la même manière que dans l'opérateur += ci-avant.
 
           Formule de la soustraction: a/b - c/d = (a*d - c*b)/(b*d)
        */
        this += -rhs;
        return this;
    }
 
    /* *=: Multiplie la Fraction côté droit à celle-ci */
    ref Fraction opOpAssign(string op)(in Fraction rhs)
            if (op == "*") {
        /* Formule de la multiplication: a/b * c/d = (a*c)/(b*d) */
        num *= rhs.num;
        den *= den.num;
        return this;
    }
 
    /* /=: Divise cette Fraction par celle côté droit */
    ref Fraction opOpAssign(string op)(in Fraction rhs)
            if (op == "/") {
        /* Formule de la division:(a/b) / (c/d) = (a*d)/(b*c) */
        num *= rhs.den;
        den *= rhs.num;
        return this;
    }
 
    /* + binaire: Produit le résultat de l'addition de cette 
       Fraction avec celle côté droit */
    Fraction opBinary(string op)(in Fraction rhs) const
            if (op == "+") {
        /* On prend une copie de cette Fraction et on ajoute 
           la fraction côté droit à cette copie. */
        Fraction resultat = this;
        resultat += rhs;
        return resultat;
    }
 
    /* - binaire: Produit le résultat de la soustraction de la 
       Fraction côté droit à cette Fraction. */
    Fraction opBinary(string op)(in Fraction rhs) const
            if (op == "-") {
        /* On utilise l'opérateur -= déjà défini. */
        Fraction resultat = this;
        resultat -= rhs;
        return resultat;
    }
 
    /* * binaire: Produit le résultat de la multiplication de 
       cette Fraction à celle côté droit */
    Fraction opBinary(string op)(in Fraction rhs) const
            if (op == "*") {
        /* On utilise l'opérateur *= déjà défini. */
        Fraction resultat = this;
        resultat *= rhs;
        return resultat;
    }
 
 
    /* / binaire: Produit le résultat de la division de cette
       Fraction par celle côté droit. */
    Fraction opBinary(string op)(in Fraction rhs) const
            if (op == "/") {
        /* On utilise l'opérateur /= déjà défini. */
        Fraction resultat = this;
        resultat /= this;
        return resultat;
    }
 
    /* Retourne la valeur de la fraction comme double */
    double opCast(T : double)() const {
        /* Une simple division. Cependant, comme la division de deux
           long causerait la perte de la valeur après la virgule, 
           nous n'aurions pas pu écrire 'num / den' ici. */
        return to!double(num) / den;
    }
 
    /* Opérateur d'ordre de tri. Retourne une valeur négative si cette 
       Fraction vient avant, une valeur positive si cette Fraction 
       vient après, et zéro si les deux Fractions sont égales. */
    int opCmp(const ref Fraction rhs) const {
        immutable result = this - rhs;
        /* comme num est un long, il ne peut être converti en 
           int implicitement. La conversion doit être faite de 
           façon explicite avec 'to' (ou cast). */
        return to!int(result.num);
    }
 
    /* Opérateur de comparaison d'égalité: retourne true si les deux 
       Fractions sont égales.
 
       La comparaison d'égalité doit être définie pour ce type, 
       autrement le compilateur en générerait une qui comparerait 
       les valeurs des membres un par un, sans se soucier des valeurs
       réellement représentées.
 
       Par exemple, bien que les deux Fraction(1, 2) et Fraction(2, 4)
       valent 0,5, le opEquals généré par le compilateur les jugerait
       inégales car leurs membres ont des valeurs différentes.*/
    bool opEquals(const ref Fraction rhs) const {
        /* Vérifier si la valeur retournée par opCmp vaut zéro est
           suffisant ici */
        return opCmp(rhs) == 0;
    }
}

précédentsommairesuivant