programmez-en-d/surcharge_operateurs.whata

1618 lines
64 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = "Surcharge des opérateurs"
partAs = chapitre
translator = "Olivier Pisano"
]
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 :
[code=d <<<
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 :
[code=d <<<
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 :
[code=d <<<
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 :
[code=d <<<
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 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 :
[code=d <<<
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 retour des opérateurs ci-après.
En coulisse, le compilateur remplace les utilisations de l'opérateur += par des appels à la fonction membre opOpAssign!"+" :
[code=d <<<
momentDuRepas += Duree(10);
// la ligne suivant est l'équivalente 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 respectées pour les types de retour.
Gardez à l'esprit que les opérateurs qui ne se comportent par naturellement sèment la confusion et sont cause de bugs.
[ = Opérateurs surchargeables
Il y a différents types d'opérateurs qui peuvent être surchargés.
[ = Opérateurs unaires
Un opérateur qui ne prend qu'un seul opérande est appelé un opérateur unaire :
[code=d <<<
++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înes de caractères correspondantes sont les suivants :
|><= Opérateur |><= Description |><= Chaîne de caractères |
| [c -objet] | Négatif (opposé numérique) | "-" |
| [c +objet] | Même valeur (ou une copie) | "+" |
| [c ~~objet] | Négation bit à bit | "~" |
| [c *objet] | Accès à la valeur pointée | "*" |
| [c ++objet] | incrément | "++" |
| [c ~--objet] | décrément | "~--" |
Par exemple, l'opérateur ++ pour ``Duree`` peut-être défini comme ceci :
[code=d <<<
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 ++ :
[code=d <<<
auto duree = Duree(20);
++duree;
>>>]
Les opérateurs de postincrément et de post-dé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 post-incrément :
[code=d <<<
/* 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 post-incrément n'a aucun coût en D si la valeur de retour du post-incrément n'est pas utilisée. C'est parce que le compilateur remplace ces expressions de post-incrément par leurs homologues préincrément :
[code=d <<<
/* 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 :
[code=d <<<
/* 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~--``.
]
[ = Opérateurs binaires
Un opérateur qui prend deux opérandes et appelé opérateur binaire :
[code=d <<<
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 coté droit |=Catégorie |
| [c + ] | Addition | [c opBinary] | [c opBinaryRight] | arithmétique |
| [c - ] | Soustraction | [c opBinary] | [c opBinaryRight] | arithmétique |
| [c * ] | Multiplication | [c opBinary] | [c opBinaryRight] | arithmétique |
| [c / ] | Division | [c opBinary] | [c opBinaryRight] | arithmétique |
| [c % ] | Reste | [c opBinary] | [c opBinaryRight] | arithmétique |
| [c ^^ ] | Puissance | [c opBinary] | [c opBinaryRight] | arithmétique |
| [c & ] | Et bit à bit | [c opBinary] | [c opBinaryRight] | bit à bit |
| [c ~|] | Ou bit à bit | [c opBinary] | [c opBinaryRight] | bit à bit |
| [c ^ ] | Ou exclusif bit à bit | [c opBinary] | [c opBinaryRight] | bit à bit |
| [c << ] | Décalage à gauche | [c opBinary] | [c opBinaryRight] | bit à bit |
| [c >> ] | Décalage à droite | [c opBinary] | [c opBinaryRight] | bit à bit |
| [c >>> ] | Décalage à droite non-signé | [c opBinary] | [c opBinaryRight] | bit à bit |
| [c ~~] | Concaténation | [c opBinary] | [c opBinaryRight] | |
| [c in] | Est contenu dans | [c opBinary] | [c opBinaryRight] | |
| [c ==] | Est égal à | [c opEquals] | | Égalité |
| [c !=] | Est différent de | [c opEquals] | | Égalité |
| [c < ] | Est avant | [c opCmp] | | Ordre |
| [c <= ] | N'est pas après | [c opCmp] | | Ordre |
| [c > ] | Est après | [c opCmp] | | Ordre |
| [c >= ] | N'est pas avant | [c opCmp] | | Ordre |
| [c = ] | Affectation | [c opAssign] | | = |
| [c +=] | Additionne et affecte | [c opOpAssign] | | = |
| [c -=] | Soustraie et affecte | [c opOpAssign] | | = |
| [c *=] | Multiplie et affecte | [c opOpAssign] | | = |
| [c /=] | Divise et affecte | [c opOpAssign] | | = |
| [c %=] | Affecte le reste de | [c opOpAssign] | | = |
| [c ^^=] | Élève à la puissance et affecte | [c opOpAssign] | | = |
| [c &=] | Affecte le réslutat de & | [c opOpAssign] | | = |
| [c ~|=] | Affecte le résultat de ~| | [c opOpAssign] | | = |
| [c ^=] | Affecte le résultat de ^ | [c opOpAssign] | | = |
| [c <<=] | Affecte le résultat de << | [c opOpAssign] | | = |
| [c >>=] | Affecte le résultat de >> | [c opOpAssign] | | = |
| [c >>>=] | Affecte le résultat de >>> | [c opOpAssign] | | = |
| [c ~~=] | Affecte le résultate de ~~ | [c 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 :
[code=d <<<
x op y
>>>]
Pour déterminer quelle fonction membre appeler, le compilateur considère les deux options suivantes :
[code=d <<<
// 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 :
[code=d <<<
x op y
>>>]
Pour l'expression ci-dessus, le paramètre ``rhs`` représenterait la variable y.
]
[ = 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 | [c collection~[i~]] |
| Affectation d'un élément | opIndexAssign | [c collection~[i~] = 7] |
| Opération unaire sur un élément | opIndexUnary | [c ++collection~[i~]] |
| Opération avec affectation à un élément | opIndexOpAssign | [c collection~[i~] *= 2] |
| Nombre d'éléments | opDollar | [c collection~[$-1~]] |
| Tranche de tous les éléments | opSlice | [c collection~[~]] |
| Tranche de certains éléments | opSlice(size_t, size_t) | [c 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) | [c ++collection~[~]] |
| Opération unaire sur certains éléments | opSliceUnary (découragée) | [c ++collection~[i..j~]] |
| Affectation de tous les éléments | opSliceAssign (découragée) | [c collection~[~] = 42 ] |
| Affectation de certains éléments | opSliceAssign (découragée) | [c collection~[i..j~] = 7] |
| Opération avec affectation de tous les éléments | opSliceOpAssign (découragée) | [c collection~[~] *= 2 ] |
| Opération avec affectation de certains éléments | opSliceOpAssign (découragée) | [c collection~[i..j~] *= 2 ] |
]
[ = Autres opérateurs
Les opérateurs suivants peuvent également être surchargés :
|= Description |=Nom de fonction |=Exemple d'utilisation |
| Appel de fonction | opCall | [c objet(42) ] |
| Conversion de type | opCast | [c to!int(objet ] |
| Transfert de fonction inexistante | opDispatch | [c objet.inexistante()] |
Ces opérateurs seront expliqués plus loin dans leur propre section.
]
[ = 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 :
[code=d <<<
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 quelques fois évitées au moyen des mixins de chaines. 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 chaine 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 :
[code=d <<<
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:
[code=d <<<
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 :
[code=d <<<
ref Duree opOpAssign(string op)(in int montant)
// ← pas de contrainte
{
mixin ("minute " ~ op ~ "= montant;");
return this;
}
>>>]
]
[ = 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``.
[code=d <<<
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 on une valeur :
[code=d <<<
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 :
[code=d <<<
int i = 1;
writeln("L'adresse de i : ", &i);
writeln("L'adresse du résultat de ++i : ", &(++i));
>>>]
La sortie contient des adresses identiques :
[code <<<
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 :
[ = 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!"++"``, ``opUnary!"--"`` et toutes les surcharges de ``opOpAssign``.
]
[ = 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``.
]
[ = Les opérateurs d'ordre
``opCmp``, qui représente <, <=, > et >= doit retourner un ``int``.
]
[ = 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``.
Note : À 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.
[code=d <<<
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 + :
[code=d <<<
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`` :
[code=d <<<
// Équivalent à l'expression ci-avant
dureeTotale = dureeVoyage.opBinary!"+"(dureeRetour);
>>>]
]
[ = 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``).
]
[ = 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``.
]
]
[ = 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) :
[code=d <<<
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é :
[code=d <<<
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 :
[code=d <<<
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 puls 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).
]
[ = 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`` :
[code=d <<<
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 :
[code=d <<<
if (x op y) { // ← op est soit <, <=, > ou >=
>>>]
Le compilateur transforme cette expression en l'expression logique logique suivante et utilise le résultat dans la nouvelle expression logique :
[code=d <<<
if (x.opCmp(y) op 0 {
>>>]
Prenons l'opérateur <=, par exemple :
[code=d <<<
if (x <= y) {
>>>]
Le compilateur génère le code suivant en coulisse :
[code=d <<<
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) :
[code=d <<<
int opCmp(in MomentDeLaJournee rhs) const {
/* Note: la soustraction est un bug 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 objet représentent le même moment de la journée.
Avertissement : Utiliser des soustractions pour implémenter ``opCmp`` est un bug 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``.
[code=d <<<
struct S {
int i;
int opCmp(in S rhs) const {
return i - rhs.i; // ← bug
}
}
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`` :
[code=d <<<
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()`` :
[code=d <<<
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 :
[code <<<
[03:40, 04:10, 09:06, 10:03, 10:09, 11:04, 13:42, 16:40, 18:03, 21:08]
>>>]
]
[ = Appeler les objets comme des fonctions : opCall()
Les parenthèses autour de la liste de paramètres lors de l'appel de fonctions 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 :
[code=d <<<
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 :
[code=d <<<
y = ax + b
>>>]
La méthode ``opCall()`` suivante calcule et retourne simplement la valeur de y selon cette équation :
[code=d <<<
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écifique a et b. Un tel objet peut être utilisé comme une fonction qui calcule des valeurs de y :
[code=d <<<
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 par la suite. Le code suivant utilise un tel objet dans une boucle :
[code=d <<<
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.
]
[ = 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 objet[index].
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 : objet[index0, index1]). 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 :
[code=d <<<
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 :
[code=d <<<
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é :
[code=d <<<
++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 :
[code=d <<<
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 :
[code=d <<<
e = deque[$ - 1]; // le dernier élément
e = deque[deque.opDollar() - 1]; // équivalent à la ligne ci-dessus
>>>]
[ = 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 necéssite 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. Au final 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 :
[code=d <<<
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 entiere 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 :
[code=d <<<
(deque ~= 100) ~= 200;
>>>]
Au final, 100 est 200 sont ajoutés à la même collection :
[code <<<
Élément à l'index 3: 3
9 7 5 3 2 55 68 4 6 8 100 200
>>>]
]
]
[ = Les opérateurs de tranchage
``opSlice`` permet 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ée).
``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[debut..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 mette 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 :
[code=d <<<
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 :
[code=d <<<
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[debuttete.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 resultat à 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 :
[code <<<
9 7 5 3 1 0 2 4 6 8
90 70 50 -1 -1 -1 -1 40 60 80
>>>]
]
[ = 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)`` :
[code=d <<<
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 :
[code=d <<<
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 :
[code <<<
double d = duree.opCast!double();
>>>]
La conversion vers double ci-dessus produit 2,5 pour deux heures et trente minutes :
[code <<<
2.5
>>>]
]
[ = 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 :
[code=d <<<
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`` :
[code=d <<<
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é :
[code=d <<<
switch (nom) {
// ...
}
>>>]
]
[ = Recherche d'inclusion avec opBinaryRight!"in"
Cet opérateur permet de définir le comportement de l'opérateur ``in`` pour les types défini 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 :
[code=d <<<
if (moment in pauseDejeuner) {
>>>]
Le compilateur utilise ``opBinaryRight`` en coulisse :
[code=d <<<
// 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 :
[code=d <<<
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`` :
[code=d <<<
if (!(a in b)) { // équivalent au code ci-dessus
>>>]
[ = 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.
[code=d <<<
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 :
[code=d <<<
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
>>>]
]
]
[ = 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 leurs précisions. 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 :
[code=d <<<
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.
[code=d <<<
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));
>>>]
]
[ = 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.
[code=d <<<
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 multiplicaton de
cette Fraction à cette 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 fait 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 example, 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;
}
}
>>>]
]