Les fonctions variadiques en C

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 de main() qui donne la taille du tableau argv ;
  • 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).

Posté par x0r à 4 commentaires • Tags : programmation c stdarg variadique variadic fonction argument variable

Commentaires

Poster un commentaire

#1 — zikmout

Top, super article merci :)

#2 — HisagiKaze

Merci beaucoup, je trouve que ces fameuses fonctions variadiques manquaient de documentations en français ! C'est bien plus clair maintenant.

#3 — 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...

#4 — 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