52. Nombre variable de paramètres▲
Ce chapitre évoque deux fonctionnalités de D qui apportent de la flexibilité aux paramètres lors des appels de fonctions :
- les valeurs par défaut de paramètres ;
- les fonctions variadiques.
52-1. Les valeurs par défaut de paramètres▲
Il peut être commode de spécifier des valeurs par défaut aux paramètres d'une fonction. Cela est similaire aux valeurs initiales par défaut des membres d'une structure.
Certains paramètres de fonctions sont la plupart du temps utilisés avec les mêmes valeurs. Par exemple, imaginons une fonction qui affiche les éléments d'un tableau associatif de type
string[string].
. Considérons que cette fonction prenne également les caractères séparateurs en paramètres :
void
afficheTA
(
in
char
[] titre,
in
string[string] ta,
in
char
[] separateurCle,
in
char
[] separateurElement)
{
writeln
(
"-- "
, titre, " --"
);
auto
cles =
ta.keys.sort;
foreach
(
i, cle; cles) {
// Pas de séparateur avant le premier élément
if
(
i !=
0
) {
write
(
separateurElement);
}
write
(
cle, separateurCle, ta[cle]);
}
writeln
(
);
}
Cette fonction est appelée ci-dessous avec « : » comme séparateur de clé et « , » comme séparateur d'éléments :
string[string] dictionnaire =
[ "bleu"
:"blue"
, "rouge"
:"red"
, "gris"
:"gray"
];
afficheTA
(
"Dictionnaire des couleurs"
, dictionnaire, ":"
, ", "
);
Ce qui produit le résultat suivant :
-- Dictionnaire des couleurs --
bleu:blue, gris:gray, rouge:red
Si les séparateurs sont presque toujours les deux même, ils peuvent être définis avec des valeurs par défaut :
void
afficheTA
(
in
char
[] titre,
in
string[string] ta,
in
char
[] separateurCle =
":"
,
in
char
[] separateurElement =
", "
)
{
// ...
}
Les paramètres par défaut n'ont pas besoin d'être spécifiés lors de l'appel d'une fonction :
afficheTA
(
"Dictionnaire des couleurs"
, dictionnaire); /* ← pas de séparateur spécifié. Les deux paramètres prendront leur valeur par défaut */
Au besoin, les valeurs des paramètres peuvent toujours être spécifiées, pas nécessairement dans leur intégralité :
afficheTA
(
"Dictionnaire des couleurs"
, dictionnaire, "="
);
Ce qui produit le résultat suivant :
-- Dictionnaire des couleurs --
bleu
=
blue, gris
=
gray, rouge
=
red
L'appel ci-dessous spécifie les deux paramètres :
afficheTA
(
"Dictionnaire des couleurs"
, dictionnaire, "="
, "\n"
);
Le résultat :
-- Dictionnaire des couleurs --
bleu
=
blue
gris
=
gray
rouge
=
red
Les valeurs par défaut ne peuvent être définies que pour les paramètres en fin de liste.
52-2. Mots-clés spéciaux comme arguments par défaut▲
Les mots-clés suivants fonctionnent comme des littéraux ayant des valeurs dépendant de l'endroit où ils apparaissent dans le code :
- MODULE : nom du module ;
- FILE : nom du fichier source ;
- LINE : numéro de la ligne ;
- FUNCTION : nom de la fonction ;
- PRETTY_FUNCTION : signature complète de la fonction.
Bien qu'ils puissent être utiles n'importe où dans le code, ils fonctionnement différemment quand ils sont utilisés en tant qu'arguments par défaut. Quand ils sont utilisés dans du code classique, leurs valeurs font référence à l'endroit où ils apparaissent dans le code :
import
std.stdio;
void
fonct
(
int
parametre) {
writefln
(
"Dans la fonction%s du fichier %s, ligne %s."
,
__FUNCTION__, __FILE__, __LINE__); // ← ligne 6
}
void
main
(
) {
fonct
(
42
);
}
La ligne 6 rapportée est dans la fonction.
Sortie :
Dans la fonction test.fonct du fichier test.d, ligne 6
.
Cependant, il est parfois plus intéressant de déterminer la ligne d'où la fonction a été appelée plutôt que l'endroit où elle a été définie. Quand ces mots-clés spéciaux sont donnés en paramètres par défaut, leur valeur fait référence à l'endroit où la fonction est appelée :
import
std.stdio;
void
fonct
(
int
parametre,
string nomFonction =
__FUNCTION__,
string fichier =
__FILE__,
size_t line =
__LINE__) {
writefln
(
"Appelé depuis la fonction %s du fichier %s, ligne %s."
,
nomFonction, fichier, ligne);
}
void
main
(
) {
fonct
(
42
); // ← ligne 14
}
Cette fois, les mots-clés spéciaux font référence à main(), l'appelant de la fonction.
Sortie :
Appelé depuis la fonction test.main du fichier test.d, ligne 14
.
En plus des mots-clés précédents, il y a aussi les mots-clés suivants, qui prennent leur valeur dépendant du compilateur et du moment de la compilation :
- DATE : date de la compilation ;
- TIME : moment de la compilation (heures, minutes, secondes) ;
- TIMESTAMP : date et moment de la compilation ;
- VENDOR : auteurs du compilateur (par ex. Digital Mars D) ;
- VERSION : version du compilateur, en tant qu'entier (par ex. la valeur 2069 pour la version 2.069)
52-3. Fonctions variadiques▲
Malgré les apparences, les valeurs par défaut des paramètres ne changent pas le nombre de paramètres reçus par une fonction. Par exemple, même si certains paramètres peuvent se voir assigner leur valeur par défaut, afficheTA() prend toujours quatre paramètres et les utilise selon son implémentation.
Les fonctions variadiques quant à elles peuvent s'appeler avec un nombre d'arguments non défini. Nous nous sommes déjà servis de cette fonctionnalité avec des fonctions comme writeln(). writeln() peut être appelée avec un nombre quelconque de paramètres :
writeln
(
"hello"
, 7
, "world"
, 9
.8
); /* et autant d'autres arguments que nécessaire */
Il y a quatre manières de définir des fonctions variadiques en D :
- celle qui ne fonctionne que pour les fonctions marquées comme extern (C). Cette fonctionnalité définit une variable cachée _argptr qui est utilisée pour accéder aux paramètres. Ce livre ne la traite pas parce qu'elle n'est pas sûre ;
- la manière des fonctions D normales, qui utilise également la variable cachée _argptr ainsi que la variable _arguments, cette dernière étant de type TypeInfo[], ce livre ne la couvre pas non plus, car elle requiert la notion de pointeurs que nous n'avons pas encore vue et qu'elle peut être utilisée de manière non sûre ;
- une fonctionnalité sûre, qui nécessite que tous les paramètres soient du même type. C'est de cela que nous allons parler dans cette section ;
- un nombre non spécifié de paramètres templates. Cette fonctionnalité sera expliquée plus tard dans le chapitre sur les modèles (templates).
Les paramètres des fonctions variadiques sont passés sous la forme d'une tranche. Les fonctions variadiques sont définies avec un seul paramètre d'un certain type de tranche immédiatement suivi par les caractères … :
double
somme
(
in
double
[] nombres ...)
{
double
resultat =
0
.0
;
foreach
(
nombre; nombres) {
resultat +=
nombre;
}
return
resultat;
}
Cette définition donne une fonction somme variadique, c'est-à-dire qu'elle peut recevoir un nombre quelconque d'arguments du moment qu'ils sont du type double ou implicitement convertibles en double :
writeln
(
somme
(
1
.1
, 2
.2
, 3
.3
));
Le seul paramètre tranche et les caractères … représentent tous les arguments. Par exemple, la tranche contiendrait 5 éléments si la fonction était appelée avec 5 doubles.
En fait, les arguments peuvent aussi être passés sous la forme d'une seule tranche :
writeln
(
somme
(
[1
.1
, 2
.2
, 3
.3
])); // identique au code précédent
Les fonctions variadiques peuvent aussi avoir des paramètres obligatoires, qui doivent être définis en premier dans la liste des paramètres. Par exemple, la fonction suivante affiche un nombre quelconque de paramètres entre parenthèses. Bien que la fonction soit ouverte quant au nombre d'éléments, elle requiert que les parenthèses soient toujours spécifiées :
char
[] parentheser
(
in
char
[] ouverture, // ← Les deux premiers paramètres doivent toujours être
in
char
[] fermeture, // spécifiés quand la fonction est appelée.
in
char
[][] mots ...) {
// ← Pas obligatoire
char
[] resultat;
foreach
(
mot; mots) {
resultat ~=
ouverture;
resultat ~=
mot;
resultat ~=
fermeture;
}
return
resultat;
}
Les deux premiers paramètres sont obligatoires :
parentheser
(
"{"
); // ← ERREUR de compilation
À partir du moment où les deux premiers paramètres sont spécifiés, les autres sont optionnels :
writeln
(
parentheser
(
"{"
, "}"
,"pomme"
, "poire"
, "banane"
));
En sortie :
{pomme}{poire}{banane}
52-4. Les paramètres des fonctions variadiques ont une durée de vie courte▲
La tranche qui est automatiquement générée pour un paramètre variadique pointe vers un tableau temporaire qui a une durée de vie courte. Ce fait n'importe pas si la fonction n'utilise les arguments que pendant son exécution. Cependant, si la fonction gardait une tranche de ces éléments pour usage ultérieur, ce serait un bogue :
int
[] nombresPourUsageUlterieur;
void
foo
(
int
[] nombres...) {
nombresPourUsageUlterieur =
nombres; // ← BOGUE
}
struct
S {
string[] nomsPourUsageUlterieur;
void
foo
(
string[] noms...) {
nomsPourUsageUlterieur =
noms; // ← BOGUE
}
}
void
bar
(
) {
foo
(
1
, 10
, 100
); /* Le tableau temporaire [ 1, 10, 100 ] n'est
* plus valide après cet endroit. */
auto
s =
S
(
);
s.foo
(
"bonjour"
, "le"
, "monde"
); /* Le tableau temporaire
* [ "bonjour", "le", "monde" ] n'est
* plus valide après cet endroit. */
// ...
}
void
main
(
) {
bar
(
);
}
La fonction foo() aussi bien que la fonction membre S.foo() sont boguées parce qu'elles stockent des tranches vers des tableaux temporaires générés automatiquement qui vivent sur la pile. Ces tableaux ne sont valides que pendant l'exécution des fonctions variadiques.
Pour cette raison, si une fonction a besoin de stocker une tranche vers les éléments d'un paramètre variadique, elle doit d'abord faire une copie de ces éléments :
void
foo
(
int
[] nombres...) {
nombresPourUsageUlterieur =
nombres.dup; // ← correct
}
// ...
void
foo
(
string[] noms...) {
nomsPourUsageUlterieur =
noms.dup; // ← correct
}
Cependant, comme les fonctions variadiques peuvent aussi être appelées avec des tranches de tableaux classiques, copier les éléments ne serait pas nécessaire dans ces cas.
Une solution qui serait à la fois correcte et efficace est de définir deux fonctions qui ont le même nom, une prenant un paramètre variadique et l'autre prenant une tranche classique. Si l'appelant passe un nombre variable d'arguments, la version variadique de la fonction est appelée, et si l'appelant passe une tranche classique, la fonction qui prend une tranche est appelée :
int
[] nombresPourUsageUlterieur;
void
foo
(
int
[] nombres...) {
/* Since this is the variadic version of foo(), we must
* first take a copy of the elements before storing a
* slice to them. */
nombresPourUsageUlterieur =
nombres.dup;
}
void
foo
(
int
[] nombres) {
/* Since this is the non-variadic version of foo(), we can
* store the slice as is. */
nombresPourUsageUlterieur =
nombres;
}
struct
S {
string[] nomsPourUsageUlterieur;
void
foo
(
string[] noms...) {
/* Comme il s'agit de la version variadique de S.foo(), nous
* devons d'abord faire une copie des éléments avant
* de stocker une tranche vers eux. */
nomsPourUsageUlterieur =
noms.dup;
}
void
foo
(
string[] noms) {
/* Comme il s'agit de la version non-variadique de S.foo(),
* nous pouvons stocker la tranche telle quelle. */
nomsPourUsageUlterieur =
noms;
}
}
void
bar
(
) {
// Cet appel est propagé vers la fonction variadique.
foo
(
1
, 10
, 100
);
// Cet appel est propagé vers la fonction qui prend une tranche.
foo
(
[ 2
, 20
, 200
]);
auto
s =
S
(
);
// Cet appel est propagé vers la fonction variadique.
s.foo
(
"salut"
, "le"
, "monde"
);
// Cet appel est propagé vers la fonction qui prend une tranche.
s.foo
(
[ "salut"
, "la"
, "lune"
]);
// ...
}
void
main
(
) {
bar
(
);
}
52-5. Exercice▲
Considérez que l'énumération suivante est définie :
enum
Operation {
addition, soustraction, multiplication, division }
Considérez également qu'il existe une structure qui représente le résultat d'un calcul et ses deux opérandes :
struct
Calcul {
Operation op;
double
premier;
double
second;
}
Par exemple, l'objet Calcul(Operation.division, 7.7, 8.8) représenterait la division de 7.7 par 8.8.
Concevez une fonction qui reçoive un nombre non spécifié de ces objets struct, calcule le résultat de chacun et retourne ceux-ci comme une tranche de type double[].
Par exemple, il doit être possible d'appeler cette fonction comme ceci :
void
main
(
) {
writeln
(
calculer
(
Calcul
(
Operation.addition, 1
.1
, 2
.2
),
Calcul
(
Operation.soustraction, 3
.3
, 4
.4
),
Calcul
(
Operation.multiplication, 5
.5
, 6
.6
),
Calcul
(
Operation.division, 7
.7
, 8
.8
)));
}
Le résultat de ce code doit être le suivant :
[3
.3
, -1
.1
, 36
.3
, 0
.875
]