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 :
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 :
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 :
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 :
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 :
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 :
- 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 :
- 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!"+" :
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 :
++
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 :
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 ++ :
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 :
/* 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 :
/* 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 :
/* 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 :
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 :
x op y
Pour déterminer quelle fonction membre appeler, le compilateur considère les deux options suivantes :
// 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 :
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 :
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 :
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 :
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 :
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.
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 :
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 :
int
i =
1
;
writeln
(
"L'adresse de i : "
, &
i);
writeln
(
"L'adresse du résultat de ++i : "
, &(++
i));
La sortie contient des adresses identiques :
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 :
- Le type de retour doit être le type de la structure, précédé du mot-clé ref qui signifie référence ;
- 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.
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 + :
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 :
// É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) :
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é :
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 :
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 :
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 :
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 :
if
(
x.opCmp
(
y) op 0
{
Prenons l'opérateur <=, par exemple :
if
(
x <=
y) {
Le compilateur génère le code suivant en coulisse :
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) :
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.
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 :
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() :
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 :
[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 :
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 :
y =
ax + b
La méthode opCall() suivante calcule et retourne simplement la valeur de y selon cette équation :
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 :
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 :
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 :
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 :
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é :
++
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 :
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 :
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 :
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 :
(
deque ~=
100
) ~=
200
;
Finalement, 100 est 200 sont ajoutés à la même collection :
É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 :
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 :
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 :
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) :
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 :
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 :
double d =
duree.opCast!
double
(
);
La conversion vers double ci-dessus produit 2,5 pour deux heures et trente minutes :
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 :
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 :
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é :
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 :
if
(
moment in
pauseDejeuner) {
Le compilateur utilise opBinaryRight en coulisse :
// 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 :
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 :
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.
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 :
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 :
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.
-
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 */
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.
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
;
}
}