Journal int *(*(*foo[])(int))(float*);

70
13
sept.
2014

Salut,

J'ai profité de mes vacances pour rédiger un petit article sur la lecture des déclarations complexes (dans le sens "obscures") en C. Ces "monstres" comme celui du titre, ne sont pas forcément fréquents, mais il peut-être utile de savoir les lire et de manière symétrique de pouvoir les construire.

Un petit rappel des déclarations est proposé autour de quelques éléments clés :

  • déclaration vs définition,
  • décomposition d'une déclaration : storage class, type qualifier, type specifier, declarator
  • les opérateurs légaux dans une déclaration (*, [], ()) et les précédences

Finalement, une méthode de lecture simple et facile à retenir est présentée. Cette méthode permet d'attaquer toutes les déclarations, aussi complexes soient-elles . Références et outils (cdecl !) concluent l'article.

Il s'agissait aussi pour moi de tester AsciiDoc (via AsciiDoctor) qui est une espèce de super Markdown et dont je ne pourrai plus me passer maintenant :-) Je ne sais pas si quelqu'un a fait une dépêche ou un journal sur AsciiDoctor d'ailleurs ? Quoiqu'il en soit, l'article est sous licence CC-by-sa. S'il peut-être utile à quelqu'un j'aurai gagné ma journée !

  • # J'ai plussé

    Posté par . Évalué à -1.

    Même si je n'ai rien compris (je ne connais pas le c ni même la programmation), j'ai plussé le journal parce qu'il parle de technique et que c'est quand même rare.

  • # ...

    Posté par . Évalué à 10. Dernière modification le 13/09/14 à 15:10.

    Sympa comme article.

    Par contre pour étoffer tu aurais pu aborder l'opérateur ','.

    int* i,j;

    'j' est il un entier ou un pointeur d'entier ?

    Il y a aussi des subtilité avec le const.

    'const char *p' vs 'char * const p'.

    • [^] # Re: ...

      Posté par (page perso) . Évalué à 10. Dernière modification le 13/09/14 à 15:47.

      Je sais pas si c'est une vraie question ou rhétorique (histoire de signaler un cas où la syntaxe est en effet peu claire), mais au cas où, je vais y répondre. :-)

      int* i,j;

      i est un pointeur sur entier, j est un entier. C'est je pense la raison pour laquelle beaucoup de projets vont plutôt écrire l'étoile à côté de la variable, non à côté du type, pour qu'on voit bien à quelle variable cela s'associe:

      int *i, j;

      Enfin si tu voulais avoir 2 pointeurs, tu écrirais:

      int *i, *j;

      Note que je n'écris dorénavant plus jamais de déclaration de cette sorte, car je trouve en effet cela abscons, et je me posais aussi la question à une époque (jusqu'à ce qu'un jour, je teste). Maintenant je déclare une variable par ligne, même si c'est le même type. Je trouve cela plus clair de cette façons. Au moins, plus de question. :-)

      const char *p' vs 'char * const p'.

      Ça a l'air d'un cas différent, mais c'est en fait très similaire à ce que tu as au dessus (en gros), c'est à dire que tu dois associer le '*' avec ce qu'il y a après.
      const char *p -> tu as d'un côté const char, de l'autre p. Donc ça signifie que p est un pointeur sur const char. Le pointeur lui-même n'est pas constant (tu peux changer la valeur de p autant que tu veux). Mais la valeur pointée par p elle ne peut pas changer (tu peux cependant faire repointer vers une autre valeur différente, ce qui a un résultat similaire).
      char * const p -> d'un côté char, de l'autre const p. Cela signifie donc que p est un pointeur sur un char (variable). Tu peux changer cette valeur pointée (le char) autant que tu veux, par contre, tu ne peux plus jamais changer le pointeur p lui-même après son initialisation.

      C'est compliqué, mais faut s'y retrouver avec des associations de ce genre.

      Film d'animation libre en CC by-sa/Art Libre, fait avec GIMP et autre logiciels libres: ZeMarmot [ http://film.zemarmot.net ]

  • # C

    Posté par (page perso) . Évalué à 7.

    Très bon article, merci. Ça donnerait presque envie de recoder en C :)

    • [^] # Re: C

      Posté par . Évalué à 4.

      Pourquoi presque ? :P

    • [^] # Re: C

      Posté par . Évalué à 10.

      Moi,cela me donne envie de continuer à programmer en C !

    • [^] # Re: C

      Posté par . Évalué à 6.

      Le C, c'est la vie :-D

      kentoc'h mervel eget bezan saotred

    • [^] # Re: C

      Posté par (page perso) . Évalué à 6.

      Comme le disais à peu près je_sais_plus_qui :

      "Le C est un formidable langage de programmation qui allie la puissance de l'assembleur à la lisibilité de l'assembleur"

      • [^] # Re: C

        Posté par . Évalué à 2.

        Comme le disais à peu près je_sais_plus_qui :

        C'est pas Benjamin Franklin ?

        • [^] # Re: C

          Posté par (page perso) . Évalué à 3.

          Nan je pense pas, lui il codait en Ada, mais il captait rien au C…

  • # lecture oui, écriture non

    Posté par (page perso) . Évalué à 10.

    Ces "monstres" comme celui du titre, ne sont pas forcément fréquents, mais il peut-être utile de savoir les lire et de manière symétrique de pouvoir les construire.

    Non, définitivement non. Il est nécessaire de savoir les lire (dans le mauvais code des autres), mais il n'est pas utile de savoir les construire (dans son propre code). Même si on les maîtrise soi-même, on ne peut être sûr des capacités des futurs relecteurs du code en question (on peut souvent s'inclure soi-même dans la liste des futurs relecteurs 6 mois après). Il vaut bien mieux 5 lignes de code simples que 1 ligne super compliquée.

    Pour la lecture de telles expressions et pour en écrire à titre éducatif ou ludique, ta doc semble très intéressante. Merci pour le temps passé à la rédiger.

    • [^] # Re: lecture oui, écriture non

      Posté par . Évalué à 5.

      Je suis bien d'accord :)
      J'imagine que certains pourraient vouloir s'en servir pour obfusquer leur code, mais comme le compilateur traite ca, ca n'a aucun impact sur le fichier généré.

    • [^] # Re: lecture oui, écriture non

      Posté par . Évalué à 6. Dernière modification le 13/09/14 à 20:48.

      Non, définitivement non. Il est nécessaire de savoir les lire (dans le mauvais code des autres), mais il n'est pas utile de savoir les construire (dans son propre code).

      Le C autorise la définition de fonction d'ordre supérieur, c'est très précieux.

      Il vaut bien mieux 5 lignes de code simples que 1 ligne super compliquée.

      Ça dépend par quoi tu accepte de remplacer la ligne compliquée.
      Je vais prendre l'exemple canonique, signal.h, qui défini une fonction signal, qui prend en paramètre un entier (le signal) et une fonction (qui prend un entier et retourne rien) et qui retourne la précédente fonction associée à ce signal.

      void (*signal(int sig, void (*func)(int)))(int);

      S'il s'agit de remplacer cette ligne compliquée par

      //en changeant la sémantique (s'exposant à des gros problèmes de concurrence):
      void (*old_fn)(int);
      void signal(int, void (*fn)(int));
      //ou bien, horrible, en affaiblissant les types :
      void * signal(int, void *);

      Alors non, pas d'accord, il faut une ligne compliquée.

      Mais, strictement équivalent (que l'on trouve dans certaine libc) :

      typedef void (*sighandler_t)(int);
      sighandler_t signal(int, sighandler_t);

      Là d'accord, c'est beau, c'est ce qu'on veut faire, et il faut savoir le faire.

      AJOUT : Z'avez vu la colorisation du C du site bug avec le type de signal, faut faire le rapport de bug où ?

      Please do not feed the trolls

      • [^] # Re: lecture oui, écriture non

        Posté par . Évalué à 3.

        Pourquoi supérieure ? C’est une fonction. Point. Une fonction qui a pour ensemble de départ un ensemble constitué de fonctions.

        Pierre, défendeur de l’égalitarisme fonctionnel, pourfendeur des mathématiques réactionnaires, matraqueur des isomorphismes.

        (Une petite larme pour ces millions de x malmenés tant et tant de fois par ces mathématiciens véreux.)

        • [^] # Re: lecture oui, écriture non

          Posté par . Évalué à 7. Dernière modification le 13/09/14 à 22:17.

          Supérieur, c'est pas méchant, mes excuses aux fonctions se sentant offensées.

          Les fonctions du premier ordre jouissent de propriétés que leurs envies beaucoup de leur sœurs d'ordre supérieur. Par exemple le fait de pouvoir vérifier que leur paramètres sont correctes, pour une fonction d'ordre supérieur c'est indécidable.

          Please do not feed the trolls

      • [^] # Re: lecture oui, écriture non

        Posté par . Évalué à 2.

        Je ne vois pas où tu veux en venir…

        D'une part, ta fonction complexe me parait aussi acceptable (le degré de complexité est modéré);
        d'autre part, tu exposes un cas élégant, qui pique moins les yeux.

        Donc quel est ton propos ? Qu'il est possible d'avoir un équivalent lisible en connaissant le language ? ;)

      • [^] # Commentaire supprimé

        Posté par . Évalué à 7. Dernière modification le 14/09/14 à 13:19.

        Ce commentaire a été supprimé par l'équipe de modération.

        • [^] # Commentaire supprimé

          Posté par . Évalué à 4. Dernière modification le 14/09/14 à 15:43.

          Ce commentaire a été supprimé par l'équipe de modération.

        • [^] # Re: lecture oui, écriture non

          Posté par . Évalué à 3.

          D'après wikipedia :

          a higher-order function (also functional form, functional or functor) is a function that does at least one of the following:

          • takes one or more functions as an input
          • outputs a function

          Un langage n'a pas besoin d'implémenter les fonctions comme citoyen de première classe pour implémenter des fonction d'ordre supérieur.

          Pour l'histoire des pointeurs de fonctions, c'est juste une question d'implémentation, ça ne diminue pas la puissance du langage. Avec le même type d'argument, on affirme que lisp ne permet pas de définir de fonctions. La preuve :

          (listp (lambda (foo)  foo))
          ; => t
          ; ce qui ressemble à une fonction n'est qu'une bête liste…

          Please do not feed the trolls

          • [^] # Commentaire supprimé

            Posté par . Évalué à 3.

            Ce commentaire a été supprimé par l'équipe de modération.

            • [^] # Re: lecture oui, écriture non

              Posté par . Évalué à 3.

              Pour écrire des fonctions prenant comme paramètre des fonctions et/ou retourner des fonctions, il faut absolument que les fonctions soient de première classe. Car il faut, que les fonctions supportent toutes les opérations courantes des autres objets : […]

              Pourquoi ? Personne n'a jamais dit qu'il était possible d'implémenter toutes les fonction d'ordre supérieur en C (surtout pas moi, comme tu (?) l'a très justement fait remarqué plus haut, il n'y a pas de fermetures en C.

              Le C permet d'implémenter des fonctions, Le C permet de passer ces fonctions implémentés à d'autres fonction.

              Voilà un programme haskell qui implémente une fonction d'ordre supérieur :

              ordre_1 :: Int -> Int
              ordre_1 i = i
              ordre_2 :: (Int -> Int) -> Int
              ordre_2 f = f 1

              Voilà un programme C, strictement équivalent au programme haskell ci dessus :

              int ordre_1(int i){
               return i;
              }
              
              int ordre_2(int f(int)){ // même pas besoin de mettre l'étoile, j'ai appris quelque chose aujourd'hui
                  return f(1);
              }

              Par quel miracle celui ci n'implémenterait pas de fonction d'ordre supérieur ?

              J'arrête de discuter là, la preuve est faite.

              Please do not feed the trolls

              • [^] # Re: lecture oui, écriture non

                Posté par (page perso) . Évalué à 1. Dernière modification le 15/09/14 à 09:09.

                C’est encore plus flagrant en Fortran, qui ne propose rien de plus que le C à ce niveau, mais dont la syntaxe cache le passage de pointeur. On a bien des fonctions d’ordre supérieur sans fermeture.

                La limitation par rapport aux fonctions citoyens de première classe est que, pour le C ou le Fortran, l’appel à la fonction qui utilise une autre fonction en paramètre n’est valide que tant que son paramètre est défini (on ne quitte pas la portée de sa définition).

                Edit: dans la mesure ou l’étoile n’est même pas nécessaire en C, le Fortran ne change rien, le passage de pointeur est discret en C aussi.

      • [^] # Re: lecture oui, écriture non

        Posté par . Évalué à 4.

        AJOUT : Z'avez vu la colorisation du C du site bug avec le type de signal, faut faire le rapport de bug où ?

        Il y a une réponse qui me vient instantanément, pour ne pas dire compulsivement, mais je vais tenter d'être plus constructif.

        https://linuxfr.org/suivi (il y a un lien en haut de chaque page)

        Catégorie, je sais pas. Syntaxe markdown, peut-être. Ou CSS. Il vaut mieux un rapport mal rangé que pas de rapport du tout.

    • [^] # Re: lecture oui, écriture non

      Posté par (page perso) . Évalué à 1. Dernière modification le 13/09/14 à 23:48.

      Je trouve ce propos impertinent.

      Je suis d'accord qu'une simplification avec un outil d'abstraction approprié comme typedef améliore à la fois la lecture et l'écriture d'une telle déclaration.
      Néanmoins il est primordial pour un bon développeur en C de maîtriser parfaitement les arcanes du typage et de la déclaration propres à ce langage, aussi brouillons et inefficaces soient-ils.

      Savoir manipuler des notions abstraites comme les fonctions "d'ordre supérieur" (qui n'en sont pas vraiment en C, pas de first-class citizenship pour nos pauvres fonctions :( ) est nécessaire à une bonne organisation d'un code complexe.

      On ne peut pas juger de la qualité du code par la complexité de déclaration des typles employés. On a affaire ici à des objets relativement simples à manipuler, qui s'expriment beaucoup plus facilement dans un langage de plus haut niveau.

      On pourrait à la limite se permettre de critiquer le langage pour son inexpressivité (qui peut être contournée, cf: typedef), mais pas l'utilisateur.

      Étant assistant professeur en C et C++, je préfère largement voir une déclaration complexe sous cette forme de la part d'un de mes élèves, qu'une décomposition en types de données intermédiaires (et les actions associées pour dégager le niveau d'indirection induit) qui ajoutent un overhead. Ça montre que l'élève à compris le typage du langage et le maîtrise. À moi d'être alerte pour reconnaître rapidement le type utilisé.

      • [^] # Re: lecture oui, écriture non

        Posté par (page perso) . Évalué à 7.

        Étant assistant professeur en C et C++, je préfère largement voir une déclaration complexe sous cette forme de la part d'un de mes élèves, qu'une décomposition en types de données intermédiaires (et les actions associées pour dégager le niveau d'indirection induit) qui ajoutent un overhead. Ça montre que l'élève à compris le typage du langage et le maîtrise. À moi d'être alerte pour reconnaître rapidement le type utilisé.

        Je te rejoins : à titre éducatif, c'est intéressant. En revanche, pour du vrai code qui sera relu par d'autres, c'est contre-productif. Dans le monde opensource ou en entreprise, tout le monde n'est pas un crack en C et il faut en tenir compte.

      • [^] # Re: lecture oui, écriture non

        Posté par . Évalué à 10.

        Le propos, c'est que l'écriture de cette forme complexifie inutilement la maintenabilité. Mais c'est peut-être une notion totalement ignorée par les organismes de formation, ceci expliquant pas mal de choses ? :) :)

        Soyons clair, on peut toujours trouver un intérêt à une chose (pédagogique, voire obscurantiste), mais en gardant un peu raison, on peut aussi s'apercevoir de sa vacuité dans 99% des cas.

  • # Ton exemple 3

    Posté par (page perso) . Évalué à 8. Dernière modification le 14/09/14 à 18:40.

    Sur ton exemple 3, tu as dans la 5e boîte ceci :

    extern int (*x[42])(int);
    ________^^^^^
    x est un tableau de 42 pointeurs vers DES FONCTIONS QUI RETOURNENT ...

    C'est pas plutôt x est un tableau de 42 pointeurs vers DES FONCTIONS PRENANT UN "int" ET QUI RETOURNENT ... ou j'ai raté quelque chose ?

    P.-S.
    Désolé, le formatage part aux fraises…

    Sed fugit interea, fugit inreparabile tempus, singula dum capti circumvectamur amore

    • [^] # Re: Ton exemple 3

      Posté par . Évalué à 3.

      C'est chouette d'avoir de relecteurs :-)
      Merci, c'est corrigé (remarque, la traduction était imprécise, mais pas fausse !)

  • # Espaces de noms

    Posté par (page perso) . Évalué à 4.

    Bravo pour ton article, au sujet duquel j'ai deux remarques à faire:

    • Tu ne parles pas des espaces de noms des diverses structures. De mémoire on peut définir différemment struct my_struct et typedef struct { } my_struct parceque dans les deux cas my_struct appartient à des espaces de noms différents. C'est un point technique qui est bien dans le sujet de ton article — mais comme je ne programme plus en C depuis longtemps je ne connais plus ces détails par cœur.

    • Tu ne parles pas du petit programme qui permet de traduire les définitions de pointeurs en anglais, c'est un exercice du K&R qui aurait toute sa place dans ton article!

    • [^] # Re: Espaces de noms

      Posté par . Évalué à 2.

      Tu ne parles pas du petit programme qui permet de traduire les définitions de pointeurs en anglais, c'est un exercice du K&R qui aurait toute sa place dans ton article!

      Tu veux dire qu’il n’en parle pas comme dans cette phrase dans la section « Liens divers » :

      cdecl : un outil en ligne de commande (pour Linux, BSD, MacOS X) permettant de traduire les déclarations dans les deux sens (anglais ⇒ C, C ⇒ anglais) ;

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.