• # Parsing en C

    Posté par . En réponse au message L'opérateur unaire * me laisse perplexe (pointeurs sur fonctions principalement). Évalué à 4.

    là, l'opérateur s'applique à ce qu'il y a à sa droite :
    int * ptr_sur_entier,entier; // déclare un pointeur sur int, et un int, donc c'est équivalent à :
    int (*ptr_sur_entier),entier;

    En fait, ça s'applique bien à gauche mais... une seule fois. :-) C'est dû au fait que contrairement aux autres spécificateurs de type, tu peux enchaîner les pointeurs sur une longueur arbitraire. Ainsi :

    typedef char *** montype;
    montype **a, *b;

    ... te donnera respectivement « char ***** a » et « char **** b ». L'opérateur « * » sert donc bien à définir un type, mais est un non-terminal.

    Pour le reste, tout devient plus clair lorsque l'on admet qu'un type est défini de manière récursive, comme lorsque tu développes une expression mathématique en commençant par les parenthèses les plus imbriquées, celles-ci pouvant se trouver au centre de l'expression et pas forcément sur les côtés.

    Un type en lui-même pourrait être défini de n'importe quelle manière mais que, dans les faits, il se trouve généralement à gauche de l'identifiant auquel il est ÉVENTUELLEMENT associé, sauf dans deux cas : les déclarations de fonctions et les déclarations de tableaux, qui se représentent respectivement avec () et [], lesquels prennent place à droite de l'identifiant. Donc, dans l'ordre :

    1. Tu poses (éventuellement) un identifiant. Le nom de ton instance ;
    2. Tu le fais suivre au choix par

    — Rien (instance ordinaire)
    — Une paire de parenthèses (fonction), à l'intérieur desquelles tu vas parser... des noms de types éventuellement nommés (les paramètres), exactement comme tu es actuellement en train de le faire. Récursivement, donc.
    — Une paire de crochets (tableau) contenant une taille éventuelle.

    1. Tu fais précéder soit par rien, soit par une une étoile pour indiquer que l'expression ENTIÈRE décrite jusqu'ici est un pointeur et non l'élément final ;
    2. Tu qualifies enfin cette expression entière en lui donnant son type propre, pour qu'elle puisse finalement être évaluée et ce, soit avec un nom de type terminal en toutes lettres, à gauche, soit en reprenant au point numéro 2 !

    Et comme le point numéro 2 est prioritaire par rapport au n° 3 et au n°4, tu peux utiliser des parenthèses dans la construction de ton expression pour forcer la priorité.

    Ainsi, une fonction admettant un int en entrée et te renvoyant un pointeur de fonction acceptant deux float et renvoyant un double se construirait ainsi :

    — « fnct » le nom de mon instance ;
    — « fnct(int) » suivie des parenthèses avec des types à l'intérieur : fonction ;
    — « * fnct(int) » précédée d'une étoile : l'expression entière est de type pointeur. Note que la priorité de 2. sur 3. fait que c'est ma « fonction qui est de pointeur » et non mon « pointeur qui pointe une fonction » ;
    — « (* fnct(int)) » ici, je suis obligé d'ajouter des parenthèses, sinon ce que je rajouterais à droite serait prioritaire sur mon pointeur. Mon expression est donc une fonction de type pointeur sur quelque chose. Mais quoi ?
    — « (* fnct(int))(float,float) » ... sur une fonction, impliquée par mes parenthèses. Cette fonction attend donc deux _float ;
    — « double (* fnct(int))(float,float) » cette fonction est une expression mathématique qui peut être évaluée. Comme elle renvoie un double, je peux placer mon terminal « double » à gauche. Mais si elle renvoyait quelque chose de plus évolué, je pourrais très bien repartir pour un tour.

    À noter ainsi que je pourrais très bien faire une fonction qui renvoie non pas un pointeur de fonction mais... une autre fonction ! Syntaxiquement, ce serait possible. Exemple : une fonction « f » qui admettrait un int et renverrait une fonction de type « int g (void) » s'écrirait :

    int f(int)(void);

    En soi, ce serait un bon moyen d'implémenter les lambdas-fonctions. Bon, en fait, cela ne pourrait pas être écrit directement de cette façon car cela impliquerait que l'on puisse instancier plusieurs fois la même fonction. Mais le C++11 a quand même introduit une syntaxe pour le faire...

    L'opérateur de transtypage, maintenant, admet tout simplement un type, sans identifiant, entre une paire de parenthèses. Il faut donc simplement résoudre ce qui se trouve à l'intérieur sans se soucier de ce à quoi on l'applique, aller au bout de la procédure, et seulement ensuite examiner ce qui suit.

    à quoi ressemble un tableau de pointeur sur fonctions qui retournent un int en mémoire ?

    1. tableau
    2. tableau[]
    3. (* tableau)
    4. (* tableau)()
    5. int (* tableau)()

    est-ce que les argument des fonctions de ce tableau changent sa représentation ?

    Ça ne change pas la manière de les construire. Par contre, quand tu définis ce type, tu déclares dans la foulée les fonctions qu'il pointe. Il est donc nécessaire d'établir proprement leur signature, pour que le programme qui déréférence le pointeur puisse utiliser les fonctions qui sont pointées.

    En outre, il y a une subtilité qui dit qu'en C ISO, fnct() et fnct(void) sont identiques mais qu'en K&R, le premier sert simplement à déclarer l'existence de la fonction sans la décrire plus. Je ne sais pas si cela s'applique sur les définitions de type.

    J'imagine que par ailleurs c'est très dépendant de l’architecture.

    En principe, non. Un code C canonique propre est censé être parfaitement portable.