samedi 23 juin 2012

Les fonctions imbriquées

Hello world !

Me voici donc pour ce premier billet de blog sérieux. Aujourd'hui, j'avais envie de vous parler des fonctions imbriquées (nested functions en anglais).

Une définition intuitive


Le mécanisme en soi est assez simple, il s'agit simplement de la définition d'une fonction à l'intérieur d'une autre fonction. Par exemple :

int
f(int k)
{
    int
    g(int n)
    {
        return 3 * n;
    }
    return 4 * g(k);
}


Ici, g est imbriquée dans la fonction f. Le principe d'une fonctions imbriquée est qu'elle est accessible par la fonction englobante et par les fonctions imbriquées dans cette fonction englobante.

Ce mécanisme peut se révéler utile, par exemple pour des problématiques de découpage de code en tâches et en sous-tâches, ou encore pour éviter de polluer l'espace de noms.

Un problème d'implémentation


Hélas, les fonctions imbriquées ne sont pas supportées par le C standard, et ceci pour une raison bien simple : que ce soit au moment de la compilation ou au runtime, ça complique beaucoup les opérations.

Concrètement, il y a deux cas de figures :
  • soit on interdit les références vers des variables locales à la fonction englobante dans les fonctions imbriquées, auquel cas les fonctions imbriquées n'auraient pas grand intérêt ;
  • soit on les autorise, et là c'est un peu plus compliqué. 

En effet, il faut faire attention d'accéder à la donnée correcte en faisant attention aux masquages. Et quid des variables allouées sur la pile dans fonction englobante ? Il faut garder ces variables dans un espace mémoire spécifique, où elles ne seront pas détruites... Et je ne parle même pas des appels récursifs et des pointeurs sur fonctions !

int
(*f)(void)
{
    int n = 42;
    int
    g(void)
    {
        return n + 1;
    }
    return &g;
}


Ce n'est pas impossible (des langages comme Algol ou D supportent correctement les fonctions imbriquées), mais ça ne rentre pas vraiment dans l'optique d'un langage natif comme le C.

Une extension gcc


Cependant, c'est une extension du langage, supportée par gcc, et activée lorsque le flag -fnested-functions est lui-même présent lors de la compilation. Le mécanisme fonctionne grâce à un trampoline.

Conclusion


Vous l'avez donc compris, le domaine des fonctions imbriquées est assez obscur, que ce soit par leur fonctionnement ou par leur lisibilité.

À mes yeux, la solution la plus propre reste d'utiliser des bonnes vieilles fonctions statiques communiquant avec quelques variables globales, ou bien en utilisant un pointeur de structure contenant le contexte en question...

Réservons les fonctions imbriquées pour des cas extrêmes !

7 commentaires:

  1. Bonjour;
    L'article me parait intéressant, j'ai une question toutefois :
    Comment utilise-t-on ces fonctions ? Il faut spécifier les paramètres de la fonctions englobante et imbriqué, mais dans quel ordre ?

    Sinon, Il nous embête trop pour laisser un com's ici. J'ai dû créer un compte gmail rien que pour ça :P

    RépondreSupprimer
    Réponses
    1. Bonjour,

      Comme indiqué au début de l'article, ces fonctions sont accessibles uniquement dans la fonction englobante ou dans les fonctions imbriquées dans la fonction englobante. À partir de cette portée, on appelle la fonction imbriquée comme une fonction classique.
      Le formatage des commentaires ne me laisse hélas pas le loisir d'insérer un code source.

      Quant à cette plateforme, elle ne me semble en effet pas forcément idéale puisque, comme tu le fais remarquer, il est difficile d'y commenter. Il faudrait peut-être que je pense à un déménagement vers quelque chose « à mon compte ».

      Merci pour ta lecture et ton commentaire.

      Supprimer
  2. Ah oui j'avais mal compris.

    Merci pour ta réponse, et bonne continuation. :)

    RépondreSupprimer
  3. Hum, ton 2nd code est invalide : déjà, il manque les arguments de f(), mais surtout tu retournes l'adresse d'une fonction locale. Shit will happen.

    Tu sembles confondre deux choses : les fonctions imbriquées, comme prises en charge par GCC, et les vraies fermetures lexicales (closures), qui peuvent échapper à leur contexte de création. Dans le cas des fonctions imbriquées, qui ne peuvent pas échapper à leur contexte, on alloue les variables sur la pile, normalement, et on utilise des techniques d'indirection (activation record chaining, ou displays). Pour les vraies fermetures lexicales, c'est plus compliqué, oui...

    RépondreSupprimer
    Réponses
    1. Justement, le but du deuxième code était de montrer que c'est erroné vis-à-vis des complications liées à la portée. En fait, il me semblait avoir ajouté un commentaire à ce propos. Vraisemblablement, cela n'a pas été fait.

      Quant aux closures, c'est un thème qui me semble différent. (mêms si on a parfois un fonctionnement similaire sur certaines bibliothèques qui utilisent des callbacks) Mais à quel niveau vois-tu une telle confusion dans l'article ?

      Supprimer
    2. Euh, bah tu dis "Et quid des variables allouées sur la pile dans fonction englobante ? Il faut garder ces variables dans un espace mémoire spécifique, où elles ne seront pas détruites..."

      Ya pas besoin de garder les variables dans un endroit où elles ne seront pas détruites, pour une fonction imbriquée, vu qu'elle ne s'exécutera que dans le contexte de la fonction parente. On alloue simplement sur la pile. C'est pour des fermetures qu'il faut déplacer des variables, éventuellement.

      Supprimer
    3. Effectivement, tu as raison sur cette phrase qui est erronée. Je corrige ça. Merci pour tes commentaires.

      Supprimer