Fermetures lexicales en C

Gazette Linux n°112 — Mars 2005

Joëlle Cornavin

Adaptation française  

Encolpe Degoute

Relecture de la version française  

Article paru dans le n°112 de la Gazette Linux de mars 2005.

Cet article est publié selon les termes de la Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières

1. Introduction
2. Imbrication de fonctions et fermetures
3. Portée
4. Pourquoi n'y a-t-il alors pas d'imbrication dans ANSI ?
5. Conclusion et complément d'informations

1. Introduction

Même si l'on est un programmeur de C expérimenté, une compilation réussie de votre programme dès la première tentative apporte sans aucun doute un sentiment de satisfaction — quelle que soit la logique sous-jacente qu'il puisse y avoir. Regardez, le compilateur C de GNU a accepté mon programme ! Jetez un coup d'œil aux deux exemples de code ci-dessous ; ce sont les méthodes les plus courantes pour commencer à coder un programme.

/* Exemple 1. */
main()
{
}
/* Exemple 2. */
#include <stdio.h>

int main()
{
        return 0;
}

Ce qui est important lorsqu'on écrit du code n'est pas l'attention mais l'approche — mais parfois, il est bon d'être sceptique au sujet de vos programmes C dans une certaine mesure. Pas nécessairement dans la logique, mais dans le compilateur que vous employez.

Le compilateur C de GNU offre plusieurs options pour compiler un bloc de code ; plus vous connaissez d'options, plus cela vous sera utile (et plus obscur). J'ai créé un alias de l'interface de GCC, cc, comme suit :


alias cc='gcc -Wall --pedantic-errors -Wstrict-prototypes'

L'option --pedantic-errors me permet de faire adhérer mes programmes C aux standards ANSI stricts. GCC fournit plusieurs extensions au langage C, qui passent souvent inaperçus ou sont considérés comme acquises, en raison des suppositions couramment admises. Je vais donc me livrer ici à une courte description d'une extension de ce type, l'imbrication de fonctions.

2. Imbrication de fonctions et fermetures

Dans l'article Functional Programming with Python, une fonction ou une procédure est dite avoir quelques analogies avec des fonctions mathématiques. Si x est une variable, alors nous avons une fonction f(x) qui effectue certaines opérations sur x pour donner une certaine valeur à y. Par conséquent, on a :


y = f(x)

Cet article sera également l'occasion d'une brève description des fermetures. Une fermeture est une propriété associée à des fonctions ; quand une fonction manipule une entrée et produit une sortie qui contient les fonctionnalités et caractéristiques de l'entrée, alors la fonction est dite posséder (nous devrions dire « satisfaire à » au lieu de « posséder ») la propriété de fermeture.

 

[ La définition ci-dessus est, peut-être, moins rigoureuse qu'elle ne pourrait l'être ; la définition standard de « fermeture » en programmation est une structure de données qui contient à la fois une fonction et un ensemble de variables définissant l'environnement dans lequel la fonction sera exécutée. ]

 
 -- Ben

Par exemple : imaginez l'ensemble de deux nombres naturels N. Si x1 et x2 sont des éléments de l'ensemble N et que la fonction f(x) est une addition (par l'opérateur binaire +) de x1 et x2, alors l'addition a la propriété de fermeture. Comme la somme de x1 et x2 et à nouveau un nombre naturel, nous pouvons affirmer que l'opérateur binaire + satisfait à la propriété de fermeture sur l'ensemble de nombres naturels.

Les langages de programmation comme Python et LISP prennent en charge l'imbrication de fonctions. L'article mentionné plus haut l'explique avec un exemple en Python. Voici un exemple concernant LISP :

(defun foo (a) (defun bar (b) (+ b 1)) (+ a (bar 3)))

(setq a (foo 4))
(print a)

La fonction bar est imbriquée et définie à l'intérieur de la définition de foo. bar incrémente et retourne le paramètre qu'il prend, et foo retourne la somme de la valeur de retour de bar invoquér avec le paramètre 3 et le paramètre qu'il admet. Voici comment la variable a est alors définie :


3 + 1 + 4 = 8

Donc, l'affichage de ma valeur a donne 8.

Cette fonctionnalité d'imbrication de fonction est perçue dans le langage C comme une extension de GCC. Compiler le code ci-dessous en ayant ajouté l'option --pedantic-errors vous indique que le « C ISO interdit les fonctions imbriquées » — mais le code effectue la compilation proprement sans l'option. Extrayez le code suivant :

/* compiler avec gcc --pedantic-errors nom_de_fichier.c*/
#include <stdio.h>
#include <stdlib.h>
int main()
{
        void foo()
        {
                printf("Hello World\n");
        }
        foo();
        return 0;
}

Comme les variables locales, l'imbrication de fonctions restreint la portée de la fonction dans laquelle elle est définie. Dans l'exemple ci-dessus; la liaison de la fonction foo n'est pas visible en dehors de main. L'association entre les identifiants et l'endroit où stocker leurs valeurs est appelé « liaison; », et la portée désigne la partie du code où la liaison de l'identifiant est visible.

Étudions l'autre exemple indiqué ci-dessous :

#include <stdio.h>
#include <stdlib.h>
int main()
{
        int x;
        x = 10;
        {
                float x;
                x = 4.2
        }
        return 0;
}

Dans l'exemple ci-dessus, x a deux liaisons par rapport à main. Mais si nous supprimons la déclaration float x;, alors la liaison sera la même partout.

3. Portée

Prenons un algorithme de recherche binaire calculé sur une liste de nombres triés. Le listing1.c. en contient le code.

Nous pouvons repérer le tableau A et la fonction binary_search vers main si nous n'avons pas d'autres fonctions qui doivent accéder à binary_search ; vous en trouverez un exemple dans le listing2.c.

Maintenant, A et binary_search sont dans la portée lexicale de main, donc enfermés dans la même portée. Définissons plus avant la notion de portée lexicale : la portée lexicale est la portée définie par la structure du code.

Un langage à portée lexicale peut prendre en charge des définitions de fonction au sein d'une autre fonction. Ainsi, la fonction imbriquée a accès aux variables locales définies dans la portée de fermeture et est elle-même visible pendant la définition de la fonction en cours d'imbrication. C'est-à-dire :

#include <stdio.h>
#include <stdlib.h>

int main()
{
        int x=10;
        void foo()
        {
                printf("Bonjour\n");
        }
        int y=20;
        void bar()
        {
                printf("monde\n");
        }
}

Ici, seule la liaison de x est visible de foo, alors que bar peut « voir » la liaison de x, foo et y. Nous pouvons maitenant affirmer que la disposition du texte détermine la portée lexicale. À présent, qu'en est-il si la fonction foo veut accéder à la fonction bar ? Une des possibilités ici serait de déclarer le prototype de bar avant la définition de foo. Observez le listing ci-dessous :

#include <stdio.h>
#include <stdlib.h>

int main()
{
        int x=10;
        auto void bar(void);
        void foo()
        {
                printf("Bonjour\n");
                bar();
        }
        int y=20;
        void bar()
        {
                printf("monde\n");
        }
        foo();
        return 0;
}

Le papier de Thomas M. Bruel sur les fermetures lexicales en C++ décrit cela comme une méthode pour autoriser une définition des fonctions mutuellement récursives à des niveaux lexicaux internes. Supprimer le mot-clé auto affichera un message d'avertissement. Essayez ! (Consultez la section «  A.8.1 in Storage Class Specifiers  » de Kernighan & Ritchie pour plus de détails).

L'autre type de portée est la portée dynamique, dans laquelle la pile d'appel active gère la résolution de nom durant le temps d'exécution. Pour clarifier, reportez-vous au listing3.c.

Dans la fonction foo, x est une référence non locale, mais elle est locale par rapport à la fonction bar. L'instruction d'affichage de la fonction main est également une référence non locale. Si le C était un langage à portée dynamique (ce n'est heureusement pas le cas), alors une référence à x dans la fonction foo serait liée à sa définition dans la fonction bar. Cependant, le C est un langage à portée lexicale et donc, une référence à x dans la fonction foo est liée à sa définition globale. Si nous lançons ce programme, la sortie sera 1, non 0.

Examinons à présent le suivant, le listing4.c : nous avons la définition de la fonction add dans la portée de init_add. Il est intéressant de noter que add désigne le paramètre i qui est passé à la fonction init_add. Pour la fonction init_add, la liaison de i est retenue (même à l'intérieur de la fonction add) jusqu'à ce que init_add retourne. Maintenant, du point de vue de la définition mathématique de « fermeture », la fonction add est dite « fermer sur » le paramètre i ; par conséquent, add satisfait à la propriété de fermeture sur i. On parle de fermeture lexicale, dans laquelle la notion de portée est préservée (la référence pour i n'est pas annulée par tout autre définition locale de i).

Il doit à présent être clair que la portée lexicale offre plusieurs avantages. Les fonctions peuvent être rendues réentrantes et en conséquence, le code machine compilé sera réentrant. Les déclarations locales peuvent être stockées dans des registres (de manière optimisée) qui élimine les références de symboles lors de la compilation (une optimisation qu'effectue le compilateur). Nous ne sommes plus limités à déclarer toutes les variables globales (une très mauvaise pratique conduisant à des problèmes comme le suicide de variables entre autres) et à passer des paramètres à chaque fonction invoquée.

4. Pourquoi n'y a-t-il alors pas d'imbrication dans ANSI ?

J'ai mentionné plus haut que l'imbrication de fonctions n'est perçue en C que comme une extension de GCC. Bien que ce soit un avantage dans une certaine mesure, nous ne pouvons pas garantir d'accès sans risque à des variables ou fonctions au sein de la portée lexicale, simplement parce que la propriété de fermeture a été satisfaite. Il ne faut pas oublier que même si la propriété de fermeture est retenue, il n'y aura dans certains cas aucune référence à une variable dans l'enregistrement actuel d'activation de la pile.

5. Conclusion et complément d'informations

Nous avons vu ce que l'imbrication de fonctions signifie et ce qu'elle peut faire dans une certaine mesure, étant donné les règles de portée lexicale de C. Si vous voulez des informations complémentaires, consultez Structure And Interpretation of Computer Programs et le Wiki sur la Lexical Closure. Pour l'article qui m'a inspiré et qui donne plus de détails sur la partie implémentation, lisez Lexical Closures For C++.

J'ai achevé ma formation de B. Tech in Computer Science & Engineering dans une petite ville appelée Trichur, dans le Kerala (Inde). Actuellement, je suis programmeur chez Naturesoft Pvt. Ltd®, à Chennai (Inde). Je consacre mes loisirs à lire et explorer des ouvrages sur Linux. Je m'intéresse également à la physique. Ma motivation dans la vie est d'aller de l'avant, plus loin, plus haut.

Adaptation française de la Gazette Linux

L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.

Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://www.traduc.org/Gazette_Linux.

Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.