Jusqu'à maintenant, tout programmeur débutant en C et ayant écrit ses propres fonctions sait qu'une fonction peut prendre un certain nombre de paramètres.
Chacune de ces fonctions ont un nombre fixe de paramètres : une fonction qui prend trois paramètres doit forcément être appelée en renseignant ces trois paramètres.
Cependant, il y a des fonctions qui semblent échapper à la règle et qui autorisent un nombre variable de paramètres. L'un d'entre eux est probablement la plus utilisée en C : il s'agit de printf. On peut donner autant d'arguments qu'on veut à printf, tant qu'on respecte les règles de syntaxe dans la chaîne de format (le premier argument à printf). Ces fonctions sont appelées fonctions variadiques pour cette raison.
On peut tout à fait écrire
printf("Bonjour le monde\n");
ou encore
printf("La réponse est %d", 42);
ou encore
printf("J'ai deux chats : %s et %s", "Mélusine", "Laptite");
Si printf le fait, alors pourquoi pas nous ? En fait, rien ne nous empêche d'écrire nos propres fonctions variadiques. Je vais donc vous montrer dans ce post comment on écrit de telles fonctions, comment on exploite un nombre variable d'arguments et quelles sont les précautions à prendre.
Fonctions variadiques, mode d'emploi
Le C est un langage qui a plus de 40 ans et la gestion des arguments variables a été standardisée dans le C89 (la fonction va_copy() a ensuite été ajoutée dans le C99). C'est pourquoi nous devons d'abord examiner les limitations de cette technique avant de commencer.
Limitations techniques
Tout d'abord, il faut savoir qu'une fonction variadique doit obligatoirement accepter au moins un argument fixe. On ne peut pas définir de fonction variadique qui puisse accepter zéro paramètre, c'est-à-dire que le code suivant ne fonctionnera pas :
void ma_fonction(...);
Il n'existe pas non plus de mécanisme qui permette de savoir si on a fini de lire la liste des arguments, ou qui permette de savoir combien d'arguments ont été passés. Il faut alors choisir l'une des deux solutions :
- on donne un paramètre qui indique le nombre de paramètres qui suivent, un
peu comme le paramètre
argc
demain()
qui donne la taille du tableauargv
; - ou alors on exige de terminer la liste d'arguments par une valeur spéciale
(une "sentinelle"), telle que
NULL
(possible uniquement lorsque la fonction prend comme argument des pointeurs).
L'une de ces deux solutions peut ne pas être possible à mettre en œuvre pour
des raisons diverses et variées. D'ailleurs, dans le cas de printf
, on part
du principe qu'il y a autant d'arguments qui suivent la chaîne de format que
de %s
, %d
ou autres dans cette chaîne, donc il peut y avoir d'autres
solutions que les deux ci-dessus.
Enfin, bien que nous ne soyions pas obligés d'utiliser les mêmes types pour
chacun des arguments d'une fonction variadique (comme pour printf
), rien ne
nous permet de savoir à l'avance de quels types sont les arguments, et rien ne
nous permet d'imposer des contraintes sur leurs types. C'est pour cette
raison que ce code compile même s'il segfaulte :
printf("%d %s", "bonjour", 42);
Comme beaucoup de choses en C, ce mécanisme fournit très peu de garde-fous et les occasions de se planter sont nombreuses si on ne fait pas attention.
Un coup d'œil au man
Pour utiliser des fonctions variadiques, il faudra inclure le fichier
stdarg.h
dans son fichier C.
La lecture de la page de man de stdarg(3) (je n'ai pas trouvé de versions en français à jour, désolé) montre que la gestion des arguments dans une fonction variadique se fait avec quatre fonctions ou macros :
- va_start(), qui nous permet de commencer à parcourir la liste d'arguments passés à notre fonction ;
- va_arg(), qui nous permet de récupérer un argument de cette liste à chaque appel ;
- va_end(), qui permet de signaler qu'on a fini de lire la liste d'arguments ;
- enfin, va_copy() si on a besoin de copier des listes d'arguments pour une raison ou une autre.
Si vous avez une machine UNIX sous la main, tapez la commande man 3 stdarg
et vous aurez le manuel sous vos yeux.
Démonstration : la liste de courses
Pour illustrer le mécanisme des fonctions variadiques, je vous propose d'utiliser comme exemple une fonction qui se contente d'imprimer à l'écran une liste de courses (donc des chaînes). Un appel à cette fonction :
courses("oeufs", "chocolat", "sucre", "farine", NULL);
devra donner la sortie suivante :
J'ai 4 choses à acheter : oeufs, chocolat, sucre et farine.
L'algorithme est relativement simple et consiste en deux étapes :
- Parcourir la liste d'arguments pour compter le nombre d'arguments passés à
la fonction (le
NULL
de la fin ne compte pas) ; - Parcourir à nouveau la liste d'arguments depuis le début pour afficher la liste de courses.
Voici donc le code, commenté de manière appropriée :
#include <stdlib.h> #include <stdio.h> #include <stdarg.h> void courses(char* arg0, ...) { va_list ap; int i, nb_args; char* cur_arg = arg0; /* On n'est jamais trop prudent */ if (arg0 == NULL) { printf("Je n'ai rien à acheter\n"); return; } /* Commencer à parcourir la liste des paramètres. La liste s'appelle ap * et le dernier argument fixe est arg0. */ va_start(ap, arg0); /* Ajouter 1 à nb_args jusqu'à ce qu'on lise un argument de type char* * égal à NULL. * Cette boucle compte le nombre d'arguments y compris le NULL, mais comme * on "oublie" de compter arg0, pas besoin de soustraire 1 du résultat * final. */ for ( nb_args = 0; cur_arg != NULL; nb_args++, cur_arg = va_arg(ap, char*) ); /* On a fini (pour cette fois) */ va_end(ap); printf("J'ai %d %s à acheter : ", nb_args, (nb_args > 1) ? "choses" : "chose"); /* Reparcourir à nouveau la liste de paramètres et les afficher un à un. * Remarquez que la boucle commence à 1 car le premier argument lu avec va_arg() * est en réalité le *deuxième* ingrédient de notre liste de courses. */ printf("%s", arg0); va_start(ap, arg0); for (i = 1; i < nb_args; i++) { if (i == nb_args - 1) printf(" et "); else printf(", "); printf("%s", va_arg(ap, char*)); } printf("\n"); va_end(ap); } int main(int argc, char* argv[]) { courses(NULL); courses("oeufs", NULL); courses("oeufs", "chocolat", NULL); courses("oeufs", "chocolat", "sucre", "farine", NULL); return 0; }
Comme on peut le voir, nous sommes déjà obligés de contourner quelques-unes
des limitations techniques de stdarg.h
pour faire une fonction aussi simple.
La première, c'est qu'on est obligés d'avoir un argument "fixe", que j'ai
appelé arg0
, pour dénoter le premier élément de notre liste de courses. Les
autres éléments sont récupérés en utilisant la fonction va_arg(ap, char*)
.
Ici, tous nos arguments sont de type char*
. La deuxième, c'est qu'on a été
obligé de compter les arguments avant de pouvoir les afficher.
En tout cas, c'est tout ce qu'il faut pour que ça marche, et lorsqu'on exécute le programme, on voit apparaître à l'écran :
Je n'ai rien à acheter
J'ai 1 chose à acheter : oeufs
J'ai 2 choses à acheter : oeufs et chocolat
J'ai 4 choses à acheter : oeufs, chocolat, sucre et farine
Conclusion
En résumé, pour utiliser les fonctions variadiques en C, la recette est la suivante :
- inclure
stdarg.h
dans son programme ; - déclarer une variable de type
va_list
; - l'initialiser avec va_start() ;
- lire les arguments un par un avec va_arg() ;
- terminer avec va_end().
Cet exemple est très simple mais je suis sûr qu'il y a d'autres usages utiles
pour ce genre de fonctions. Personnellement, je m'en sers le plus souvent
pour faire un wrapper autour de printf
(ou une fonction qui se comporte que
la même manière que printf
) dans des situations où on n'a pas directement
accès à stdout et stderr (lorsqu'on réalise une application SDL en plein
écran par exemple).
Commentaires
Poster un commentaire
zikmout
Top, super article merci :)
HisagiKaze
Merci beaucoup, je trouve que ces fameuses fonctions variadiques manquaient de documentations en français ! C'est bien plus clair maintenant.
vi
merci pour les explications. J'aurai cependant une question. Comment tu fais si tu ne sais pas quel est le type des arguments qui sont passes dans l'ellipse? parce que la tu definis un char*, mais si apres j'ai un int, puis un char etc...
★ x0r
La fonction n'est pas en mesure de connaître dynamiquement les types des arguments de l'ellipse. Dans mon exemple, si tu passais des int ou d'autres types que char*, ça fera un segfault parce que j'ai supposé que tous les paramètres donnés à ma fonction sont des char*.
En revanche, il est possible de contourner le problème en s'inspirant de ce que fait printf().
En effet, la chaîne de formatage détermine comment printf() interprète chaque argument : un « %s » entraîne l'interprétation de l'argument suivant comme un char*, un « %d » ou un « %x » entraîne son interprétation comme un int, et ainsi de suite.
S’il y a besoin d'interpréter chaque argument de l'ellipse sous un type qu'on ne peut pas déterminer statiquement, il faut donc que les types de ces arguments puissent être connus de la fonction à l'aide d'un argument obligatoire. Une idée serait de passer comme argument obligatoire une chaîne de caractères dont chaque lettre symbolise le type de l'argument de l'ellipse. Alors, si on suppose qu'un « s » indique une chaîne de caractères et un « d » un int, alors on pourrait envisager une fonction foo() qui soit appelé de cette manière : foo("ssds", "a", "b", 42, "c").
Évidemment, si on ne respecte pas l'interface, on risque au mieux un résultat faux et au pire un segfault. Comme je le disais, le mécanisme de fonctions variadiques en C ne propose quasiment aucun garde-fous.
Poster un commentaire