[Atom] [Mail] [Twitter]
Liens : git · hacks · divers · cabale · buzz! · à propos +
Au menu
\/

Pourquoi le C est moins puissant que votre langage favori

#. Par rz0 dans Le code et ses raisons. Publié le 27.03 2010 à 02:38. 3 commentaires.
<. Le code et ses raisons : typedef en C
c débutants langages

On entend souvent dire que l’on peut tout faire dans un langage comme dans un autre, Turing-complétude, tout ça. La variante de ce discours est que l’on peut tout faire en C parce que c’est un langage de bas niveau. Certes, on peut tout faire, mais on ne peut pas tout faire aussi bien, c’est-à-dire aussi efficacement.

Ainsi, avant de poursuivre avec mes articles sur les techniques de programmation en C, j’ai décidé de prendre le temps d’écrire un court billet sur ce que l’on ne peut pas faire en C. Ce petit texte n’a pas pour prétention d’être exhaustif, car la liste de ce que l’on ne peut pas faire en C est sûrement longue, très longue. Mais je souhaite donner ici quelques points de réflexion et principes de base, pour les plus débutants d’entre nous.

Le problème

Pourquoi donc ne peut-on pas faire aussi efficacement en C certaines choses que l’on peut bien faire dans un autre langage ? La réponse tient en cela : l’abstraction.

L’argument que l’on entend souvent est le suivant : le C étant plus bas niveau, il suffit de recoder les mécanismes internes abstraits par tel ou tel langage de plus haut niveau. Je suis d’accord avec cette méthode, je l’aime même beaucoup, étant moi-même assez spécialisé dans l’implémentation des langages. Mais il est important d’en connaître les limites.

Les limites, ce sont les limites de définition du langage. Si l’on veut utiliser le C comme un langage raisonnablement portable, et que l’on s’en tient à la norme, on hérite par la même occasion de contraintes normées, plus fortes que celles imposées par la plateforme pour laquelle on développe.

Concrètement, cela signifie que sur une machine donnée, votre beau C portable ne pourra pas recourir aux mêmes astuces que l’implémentation d’un langage de plus haut niveau (qui, elle, n’est pas portable). Prenez par exemple les variables de Scheme, ou tout autre langage dynamique. Dans ces langages, une variable peut pointer sur un objet, ou contenir une valeur numérique unboxed. En profitant de la représentation des pointeurs au sein du système hôte, et du fait que la mémoire n’y est jamais allouée que sur un alignement de 2, on peut utiliser un bit pour déterminer la nature de l’objet (boxed ou unboxed).α C’est malin comme tout, et vieux comme le monde. Mais inapplicable en C. Question de portabilité. En effet, on n’a même pas la garantie que les pointeurs soient convertibles en entiers !

Mais, mais, me direz-vous, en Scheme non plus, on n’a pas cette garantie ! Oui… mais en Scheme, il n’y a pas de pointeurs comme en C ! Autrement dit, le programmeur s’en contrefiche : il utilise simplement son langage, et c’est au compilateur de décider comment telle ou telle fonctionnalité est traduite au niveau de la machine ; du point de vue du langage, on a perdu…

α : Ce n’est pas un article sur l’implémentation des langages, voyez la page Wikipédia sur l''unboxing', si vous n’êtes pas à l’aise avec ces notions.

La solution ?

Mais on n’a qu’à implémenter des solutions spécifiques en plus de la version générale moins efficace ! C’est en effet une possibilité.

Par exemple, pour les objets dynamiquement polymorphes pointeurs / entiers, on pourrait se définir un petit jeu de macros dans ce genre-là :

#include <stdint.h>

#if UINTPTR_MAX && HAVE_2ALIGNPTRS

typedef uintptr_t VariantInt;
typedef uintptr_t Variant;

#define ISINT(x) ((x) & 0x1)
#define INTVAL(x) ((x) >> 1)
#define PTRVAL(x) ((void *)(x))
#define SETINT(x, y) ((x) = (y) << 1 | 0x1)
#define SETPTR(x, y) ((x) = (uintptr_t)(y))

#else

#if UINTPTR_MAX
typedef uintptr_t VariantInt;
#else
typedef long VariantInt;
#endif
typedef struct {
        union {
                VariantInt _int;
                void *_ptr;
        } _val;
        unsigned _isint: 1;
} Variant;

#define ISINT(x) ((x)._isint)
#define INTVAL(x) ((x)._val._int)
#define PTRVAL(x) ((x)._val._ptr)
#define SETINT(x, y) ((x)._val._int = (y), (x)._isint = 1)
#define SETPTR(x, y) ((x)._val._ptr = (y), (x)._isint = 0)

#endif

Bref, un truc du genre. Pas le plus beau jeu de macros du monde, mais vous comprenez le principe.

Et vous vous attendez sans doute maintenant à ce que je réfute cet argument… et bien non ! En réalité, c’est une manière parfaitement viable d’étendre un peu le langage. Cependant, elle requiert quelques précautions.

  • Elle demande d’être méthodique, en cela qu’il faut patiemment écrire une macro (ou une fonction) pour abstraire chaque opération affectée par le changement d’implémentation. Cela peut être long, fastidieux, et si l’on teste plus une implémentation qu’une autre, on court le risque de se laisser biaiser, et d’oublier d’isoler des fonctionnalités qui devraient l’être.

  • Elle tend à faire basculer les meilleurs programmeurs du côté obscur de la non-portabilité. :-° En effet, certains hacks marchent tellement biens que l’on est vite tenté de se dire qu’il ne sert à rien de maintenir une version générique, sous-optimale, mais conforme à la norme. Et c’est là un gros risque ! Parfois, on peut effectivement se laisser aller…

    C’est une question subjective, et je ne peux certainement pas décider à votre place. Je ne peux que vous offrir une règle générale que je m’applique de manière plus ou moins stricte :

    Plus le code se veut général (bibliothèques, composants réutilisables), plus il est important de maintenir une version standard, portable, de l’implémentation.

    Un avantage de cette stratégie est que vous pouvez en toute tranquillité utiliser l’implémentation simple, non optimisée, par défaut, et doucement migrer les différents systèmes, au cas par cas, vers votre code spécialisé, au fur et à mesure de vos tests (ou de ceux de vos utilisateurs !).

    Mais bien sûr, il ne faut pas se focaliser sur la portabilité, qui, de toute manière, est toute relative. En effet, si, par exemple, votre application dépend déjà fortement d’une bibliothèque tierce, telle que la GLib, qui elle-même fait certaines hypothèses sur l’implémentation, il peut être raisonnable de vous appuyer dessus.

Que retenir de tout ça ?

Au final, je dirais, pas grand chose, si ce n’est que j’essaierai, pour ma part, d’être précis quant aux implications des méthodes que je décris. Ce n’est pas (seulement) pour être pédant ; je pense qu’il est réellement important de comprendre (pour mieux ignorer, dirons certains) les limites des définitions et des standards que l’on accepte, parfois sans le dire.

À bientôt donc, pour de nouvelles aventures ! :)

[ tag:blog.huoc.org,2009:posts/47 ]
Voir les commentaires · Commenter

/\ \/

7 recettes pour aller plus loin avec le préprocesseur C

#. Par rz0. Mis à jour le 15.03 2010 à 17:07. 9 commentaires.
c effets_de_bord généricité préprocesseur trucs_et_astuces

Ce soir, j’ai décidé d’être paresseux, et comme beaucoup de blogueurs, je vous sers ici un peu de précuit, un peu de réchauffé : une liste de trucs et astuces !

Mais pas n’importe laquelle ! Récemment, j’ai twitté sur le triste état des ressources disponibles sur le Web français apparaissant dans des recherches telles que « généricité C ». Il y a beaucoup trop de cours et autres tutos superficiels, destinés à inculquer aux débutants quelques bases du langage ou de la programmation. J’ai donc décidé de réagir, à mon échelle, en publiant sur mon modeste blog des articles sur le C pour les bons ! :-° Voici donc le premier : le préprocesseur C, pour les bons !

Je ne saurais être tenu responsable de l’utilisation que vous faites des techniques décrites ici. En particulier, si vous vous faites insulter pour code illisible ou quelque chose comme cela, il ne faudra pas venir vous plaindre. :]

Si en lisant ce qui suit, vous ne vous sentez pas très à l’aise, ou ne comprenez pas quelque chose, il vous manque peut-être quelques notions de base ; mais pas de panique, il vous suffit d’aller lire un cours quelconque, comme par exemple ce tuto dédié au préprocesseur, sur le SdZ.

La plupart des astuces que je présente ici sont illustrées dans des frameworks tels que COS. En moins violent, et plus usité, les en-têtes BSD `sys/queue.h` et `sys/tree.h`, dont je reparlerai probablement bientôt, sont également de bonnes illustrations de certaines techniques présentées ci-dessous. Citons aussi SGLIB, dans la même veine que sys/queue.h et sys/tree.h, mais poussant le concept un peu plus loin.

J’ai classé les astuces par ordre croissant de degré d’aliénation requis pour accepter de les utiliser. :-° Prêts ? Let’s rock!

Générez des séquences : opérateurs ,, ?:, && et ||

On vous a toujours dit que l’opérateur ,, c’était mal, que c’était Le Mal, après ‘goto‘. Mais il y a un cas où celui-ci peut s’avérer être un allié… intéressant. Il s’agit du contexte des macros.

Bien souvent, vous aurez envie que votre macro se comporte le plus possible comme une fonction, c’est-à-dire que si celle-ci renvoie une valeur, vous pouvez l’utiliser dans une expression.

C’est là que l’opérateur virgule (,) entre en jeu : il vous permet de placer plusieurs expressions dans votre macro, et que le tout soit réutilisable… comme une expression ! Les opérateurs ?:, && et || ajoutent un peu de variété à votre éventail de possibilités… mais rappelez-vous qu’il faut employer avec && ou || des opérandes à valeur entière (ou en tout cas qu’il est possible de convertir en entier).

Et un exemple bidon :

#define GETARG(argcptr, argvptr) (--*(argcptr), ++*(argvptr))
#define CMP(p, q) ((p) == (q) || cmpfunc((p), (q)) == 0)

Remarquez le problème des effets de bords potentiellement provoqués par l’évaluation de p ou q, dans la seconde macro ; nous allons y revenir…

Générez des structures de contrôle : utilisez la boucle for !

Mais les macros ne sont pas limitées à remplacer des fonctions, elles peuvent également être utilisées pour créer de nouvelles structures de contrôle.

À la différence d’un appel de fonction, l’utilisation d’une structure de contrôle inclut un (ou plusieurs) blocs de code. Le schéma de base simplifié est le suivant :

BEGIN (/* ... */) {
        /* ... */;
} END (/* ... */);

Il faut bien sûr remplacer BEGIN et END par des macros appropriées.

En utilisant des boucles for pour implémenter votre structure de contrôle, vous pouvez souvent omettre la partie END, et ainsi alléger l’usage de vos macros.

Un exemple de telles constructions peut être trouvé dans sys/queue.h ; dans cet exemple, on parcourt une liste (implémentation typique : une boucle for) :

LIST_FOREACH (var, head, next) {
        /* 'var' pointe successivement sur chaque élément. */
        /* ... */;
}

Un autre exemple, de structure plus complète, munie du marqueur de fin, peut être trouvé dans les bibliothèques standards de Plan 9 ; le code suivant permet de traiter les options de la ligne de commandes :

ARGBEGIN {
case 'a':
        /* Option '-a' spécifiée. */
        /* ... */;
        break;

case 'b':
        /* ... */;
        break;

default:
        usage();
} ARGEND;

Utilisez la concaténation pour simuler la généricité par nom

La concaténation est un mécanisme puissant puisqu’elle permet de générer des identificateurs à partir de fragments, dont certains peuvent être passés en paramètres à vos macros.

Moyennant le respect de quelques conventions dans les noms, vous pouvez écrire du code générique, dont les parties spécifiques sont masquées derrière un espace de nom improvisé passé en argument.

Et encore un exemple bidon pour illustrer le principe de base :

#define RELEASE(name, p) do {                               \
        if (name##_decrref(p) == 0)                         \
                free(p);                                    \
} while (/* CONSTCOND */ 0)

Générez des définitions

Rappelez-vous l’histoire des effets de bord que nous avons rencontrée plus haut. Il n’y a pas de méthode magique, pour éviter les effets de bords, il faut passer par des variables intermédiaires.

Une solution simple et plutôt élégante pour résoudre ce problème est de générer non plus du code directement, mais des fonctions… Mais, mais, me direz-vous, quel intérêt de passer par des macros pour générer des fonctions ? Pourquoi ne pas écrire les fonctions directement ?

Et bien, cette astuce, combinée à la précédente, permet un peu de généricité ! Et un ptit exemple pas très utile, pour la route :

#define ARRAY_FIND_PROTOTYPE(name, type, cmp)                 \
type *name##_ARRAY_FIND(type *, size_t, type *)

#define ARRAY_FIND_GENERATE(name, type, cmp)                  \
type *name##_ARRAY_FIND(type *_a, size_t _n, type *_elm)      \
{                                                             \
        for (; _n > 0; ++_a, --_n) {                          \
                if (cmp(_a, _elm) == 0)                       \
                        return _a;                            \
        }                                                     \
        return NULL;                                          \
}

#define ARRAY_FIND(name, a, n, elm)                           \
        name##_ARRAY_FIND((a), (n), (elm))

Pour des exemples plus complets, je vous invite à jeter un coup d’œil à l’implémentation de `sys/tree.h` (ou localement, dans /usr/include/sys/tree.h si vous avez un BSD sous la main).

Côté performances, vous n’avez guère de soucis à vous faire. Si vous définissez des fonctions statiques, le compilateur s’occupera tout seul comme un grand de les machiner comme il se doit. Et si vous voulez lui forcer un peu la main, inline est là pour ça.α

α : Mais c’est du C99… je reviendrai probablement sur cette question dans un prochain article.

Simulez les alternatives avec la concaténation

Nous arrivons ainsi à la dernière astuce accessible avec un préprocesseur à la norme C89.

Vous connaissez certainement les directives #if, #ifdef, et compagnie. Hélas, elles ne peuvent être utilisées à l’intérieur d’une définition de macro.

Dans les cas simples — mais très courants — où vous souhaitez simplement discriminer entre plusieurs valeurs d’une constante passée en paramètre, cependant, vous pouvez vous en sortir en utilisant… la concaténation ! Encore elle !

En effet, il suffit pour cela de définir chaque alternative comme une macro séparée, nommée avec un préfixe commun, et un suffixe dépendant du cas. Illustration :

#define DECL_STATIC_0
#define DECL_STATIC_1 static
#define DECL_STATIC(s) DECL_STATIC_##s

#define SOME_FUNCTION_PROTOTYPE(name, type, s)              \
DECL_STATIC(s) type some_function(type);

Simulez les opérations sur les n-uplets avec les macros variadic (C99)

Avec C99 ont été introduites les macros variadic, c’est-à-dire acceptant un nombre variable d’arguments. Cet ajout, a priori mineur, est en vérité très important, car il permet la manipulation des n-uplets, soit des collections de valeurs.

Pour nous, un n-uplet sera une suite d’éléments séparés par des virgules, entre parenthèses. Par exemple :

(a, b, c)

Pour opérer sur des tuples, le truc de base à remarquer est le suivant :

  • les appels de macros respectent l’équilibrage des parenthèses (ce qui permet effectivement de passer des n-uplets comme simples arguments à des macros) ;

  • les macros sont développées autant de fois que possibles : si le texte substitué par une macro contient encore des invocations de macros, celles-ci sont traitées (sauf si elles ont déjà été appelées) ;

  • en juxtaposant un n-uplet à un nom de macro, on obtient un appel de la macro correspondante, avec en guise d’arguments les éléments du n-uplet !

Il faudrait un billet entier pour explorer en profondeur toutes les possibilités offertes par cette astuce. Je ne vais présenter ici qu’un cas d’utilisation simple, mais sachez qu’il est possible, par exemple de créer une macro qui substitue tout n-uplet par 1 et toute autre construction par 0, par exemple ! Je vous laisse imaginer ce que l’on peut en faire, combinée aux alternatives par concaténation expliquées ci-dessus.

Mais revenons à notre modeste exemple, qui illustre la fonction identité de manière ridiculement complexe :

#define IDENTITY(...) __VA_ARGS__

/*
 * 'IDENTITY' peut en fait servir à supprimer des parenthèses
 * superflues gênantes.
 */
#define CALL_WITH_RESOURCE(name, var, aargs, f, args) do {  \
        name##_type var = name##_alloc aargs;               \
        f(var, IDENTITY args);                              \
        name##_free(var);                                   \
} while (/* CONSTCOND */ 0)

#define FILE_type FILE *
#define FILE_alloc fopen
#define FILE_free fclose

/* ... */
CALL_WITH_RESOURCE (FILE, fp, ("log.txt", "a"),
    fprintf, ("%d\n", 42));

COS contient une bibliothèque entière de macros travaillant sur les n-uplets. Je vous invite à regarder le code source si cela vous intéresse ; il s’agit plus particulièrement du dossier CosBase/include/cos/cpp/, dans l’archive.

Énumérez les cas et déroulez les appels pour émuler la récursion (C99)

Au vu de ce que nous venons de voir, vous êtes en droit de vous demander : « Peut-on faire pire ? » Et la réponse est oui ! :) J’ai gardé cette astuce pour la fin car elle constitue, à mes yeux, une limite que je ne souhaite pas, à titre personnel, franchir.

Un peu plus haut, j’ai dit quelque chose d’important : dans une chaîne de substitutions de macros, une macro déjà substituée ne le sera plus, même si elle apparaît dans le texte produit… il est donc impossible de faire des macros récursives !

L’astuce ici consiste à dire : « L’univers est fini, je vais décrire tout l’univers ! » Aidée de la concaténation, les possibilités sont vraiment (in)finies ! Mais je ne m’étendrai pas davantage sur le sujet. Encore une fois, COS contient toute une panoplie d’exemples, qui, je n’en doute pas, apparaîtront brillants pour certains, et affligeants pour d’autres.

Voilà, en espérant vous avoir appris quelques petits trucs rigolos ! Have fun!

[ tag:blog.huoc.org,2009:posts/46 ]
Voir les commentaires · Commenter

/\ \/

Le code et ses raisons : typedef en C

#. Par rz0 dans Le code et ses raisons. Mis à jour le 27.03 2010 à 02:40. 3 commentaires.
<. Le code et ses raisons : goto en C
c débutants idiomes pratique programmation typedef

Voici un vieil article qui traînait au fond de mes dossiers. Comme il semble presque terminé, je me suis dit que j’allais le poster tout de même…

Aujourd’hui donc, au menu, typedef, le bien nommé, qui, très naturellement, permet de définir un type, c’est-à-dire lui associer un nouveau nom. En pratique, toutefois, les choses ne sont pas si simples et il est bon de rappeler quelques règles d’utilisation fondamentales avant d’attaquer la partie plus « philosophique » de notre billet :

  • typedef s’utilise comme une classe de stockage, dans une déclaration classique ;

  • à ce titre, un nom de type ainsi défini ne peut l’être qu’une seule fois dans une unité de compilation (fichier C plus en-têtes inclus) ;

  • enfin, l’identificateur résultant de cette opération est un synonyme du type utilisé : l’ancien et le nouveau type sont utilisables de manière interchangeable.

typedef, pourquoi

Il y a maintes raisons pour lesquelles quelqu’un peut vouloir utiliser typedef, mais il est possible de les ranger en deux familles :

  • celles qui visent à simplifier l’écriture : p.ex. en renommant un pointeur de fonctions, ou en permettant d’omettre le mot-clé struct ou union ;

  • et celles qui introduisent une abstraction (p.ex. un type entier de taille variable selon l’architecture, ou encore un type opaque).

Les deux usages ont leurs défenseurs et, dans une certaine mesure, leurs détracteurs. Si le second emploi ne semble pas contestable en tant que tel, c’est plutôt la nécessité de l’abstraction qui est souvent remise en cause. Et si le premier apparaît comme une puissante manifestation de flemme, l’histoire a montré que de grands noms le soutiennent.

On peut citer à cet effet Bjarne Stroustrup et son C++, dans lequel la déclaration d’une variable de type structure ou union ne requiert pas de mot-clé particulier.

typedef abusif ?

Il y a une école, relativement répandue, notamment dans le milieu scolaire, dont la position est de systématiquement créer un synonyme pour toute structure ou union définie. Et, à l’inverse, il existe certains cercles où cette pratique est violemment condamnée.α Elle est entre autre explicitement déconseillée par le KNF d'OpenBSD.

La raison invoquée est la suivante : utiliser typedef sur une structure engendre une abstraction non toujours voulue. Lorsque celle-ci est conçue comme un simple agrégat de données, et ses membres pensés pour un accès direct, typedef masque la nature du type réel à l’utilisateur.

Dans l’autre camp, la parole est à l’uniformité. En effet, il existe une multitude de niveaux, ou plutôt de formes, d’abstraction, au-delà du simple qualificatif opaque, et son contraire. À partir de là, en quoi est-il légitime de différencier (et à quel niveau cette distinction doit-elle se faire) la structure directement accessible du type abstrait ? Si un objet a une partie de ses membres accessibles, doit-il utiliser le mot-clé struct ?

α : Pour information, je suis également de ceux qui s’y opposent.

Zoom sur l’idée d’abstraction

Bien plus intéressant que les questions de raccourcis d’écriture, la notion d’abstraction est celle qui domine réellement ce débat. S’abstraire, mais de quoi ?

On peut s’abstraire du matériel, tout d’abord, ou plus généralement de la plateforme, du système : la définition de types (le plus souvent entiers) susceptibles de changer d’une architecture à une autre est une pratique commune, connue depuis longtemps sous Unix et en C, en général. Les diverses normes en contiennent elles-mêmes une collection importante : size_t en ANSI-C, off_t dans POSIX, ou encore les fameux intXX_t du C99.

Au niveau au-dessus, on peut vouloir s’abstraire de l’implémentation. Par exemple, il existe de nombreuses façons d’écrire un dictionnaire, mais l’interface basique est la même : on peut y chercher, insérer et supprimer. Nommer son type Dict est une manière courtoise d’indiquer ses intentions : on n’a besoin que de cette interface-ci.

Typiquement, on se retrouve alors avec un type concret, disons struct htable, qui modélise une table de hachage, que l’on souhaite masquer derrière le type Dict. La bonne pratique du C veut que l’on passe en paramètre de nos fonctions un pointeur vers struct htable. La question qui divise est alors la suivante : faut-il utiliser typedef la structure ou le pointeur vers celle-ci ?

Ça a l’air idiot, n’est-ce pas ? Pourtant, il n’y a pas vraiment de bonne réponse à cette question… et pour s’en convaincre, essayons d’entrer dans le raisonnement des deux partis.

La question fondamentale est : faut-il exposer la structure de pointeur ? Si l’on redéfinit le type structure alors on garde la structure de pointeur dans toutes nos interfaces ; si l’on redéfinit le type pointeur, alors les interfaces ne montreront qu’un objet complètement opaque.

Dans un camp

Un premier point de vue est de dire que l’objet se comporte comme un pointeur (car c’est un pointeur !) donc l’analogie est justifiée. On pourrait dire que cela restreint l’implémentation (on pourrait vouloir utiliser une sorte d’ID quelconque). Mais étant donné que la majorité des implémentations raisonnables, en C, utilisent une forme ou une autre d’objet en mémoire dont on ne manipule qu’une référence (le pointeur), la limitation est relativement virtuelle.β

De plus, on peut aussi décréter que les types abstraits se manipulent comme des types scalaires (entiers, pointeurs, etc.) et donc finalement partagent plus ou moins toutes les propriétés de ceux-ci (égalité, affectation, passage en argument, etc.). De ce point de vue, les types obtenus en redéfinissant le type pointeur vers structure se mêlent mieux aux types de base du C, tandis qu’en redéfinissant le type structure lui-même, les opérations disponibles sur celui-ci sont limitées ; en général, il n’est même pas permis de déclarer une variable de ce type (seulement du type pointeur associé).

β : Le seul domaine que je vois où elle pourrait devenir réellement contraignante est la programmation système : le noyau conserve souvent un certain nombre d’informations sous forme de tables indexées, plutôt que de pointeurs vers des objets directement en mémoire, ces objets n’étant pas forcément situés (entièrement) dans l’espace mémoire de l’utilisateur.

Cependant, il peut arriver qu’il soit plus commode de manipuler les indices au lieu des pointeurs eux-mêmes, notamment si l’identificateur entier est réutilisé pour indexer d’autres structures.

Et dans l’autre

Cependant, si l’on opte pour le typedef simulant un type scalaire, quelque part, on dissimule une information capitale : chaque objet de ce type en cache un autre, « plus gros ». En d’autres termes, ce ne sont que des références.

À l’inverse, en conservant explicitement la syntaxe des pointeurs, cette notion est clairement identifiée et passer un pointeur à une fonction souligne le potentiel de celle-ci à modifier une donnée tierce, masquée par le pointeur.

À cela, les défenseurs de l’autre camp pourront rétorquer qu’une structure de données complexe cache en général elle-même de multiples niveaux d’indirections, et qu’ainsi il est tout aussi trompeur de faire croire à l’utilisateur que c’est l’objet directement pointé qui sera sujet à modification : bref, que l’on ne présente ainsi que le haut de l’iceberg.

Bien sûr, il est facile de considérer que la syntaxe n’est qu’un indice conventionnel signalant au programmeur que de la mémoire indirectement manipulée entre en jeu. Tout comme il est facile d’y opposer le fait qu’à l’usage cela est invisible (le typage étant réservé au prototype) et que la documentation a déjà à sa charge ce genre de détails, ou encore que d’autres techniques sont plus appropriées (p.ex. l’usage d’un qualificateur de fonctions pure tel qu’on le trouve dans GCC). On pourrait contre-attaquer en remarquant que le typage devrait alléger la documentation, à quoi on pourrait répondre que celui du C est de toute façon trop faible pour cela… J’aime débattre avec moi-même ; tout cela n’est qu’opinion, au final. :)

Conclusion

Malgré toutes ces années à coder en C, je ne suis pas parvenu à forger un avis prononcé sur la question. J’oscille moi-même entre les diverses opinions, à mesure que de nouveaux argumentaires en faveur d’une pratique ou d’une autre me parviennent…

Ma position actuelle, si elle intéresse quelqu’un, relève d’un point de vue que je qualifierais de « minimaliste » : je me suis fixé, pour mes nouveaux programmes, de n’utiliser typedef que lorsque j’y vois un intérêt immédiat (p.ex. pour la portabilité des types entiers). Le raisonnement derrière cette politique est que l’abstraction est, en C et au niveau où je travaille, relativement pauvre, et qu’un changement d’implémentation ne saurait généralement se passer d’une altération de l’API tout entière. Abstraire seulement le typage ne suffit pas. Abstraire davantage entraîne un risque de complexité accrue et de performances moindres.

Mais je ne suis pas pour autant fondamentalement opposé à l’usage de typedef dans d’autres scénarios. Une bibliothèque de très haut niveau qui reposerait sur la manipulation d’objets (au sens POO) pourrait choisir de nommer ses classes avec typedef. Une telle convention aurait du sens.

Je concluerai donc sur ces mots : il faut ainsi prendre en compte le contexte et établir des conventions. Pour rabâcher le principe de style fondateur et bien connu : après le bon sens, la cohérence doit primer.

[ tag:blog.huoc.org,2009:posts/37 ]
Voir les commentaires · Commenter

/\ \/

Le code et ses raisons : goto en C

#. Par rz0 dans Le code et ses raisons. Publié le 10.09 2009 à 13:06. 14 commentaires.
<. Conventions : le retour
>. Le code et ses raisons : typedef en C
c débutants goto idiomes pratique programmation

Tak tak, aujourd’hui Ours répond ! Quelqu’un m’a demandé hier pourquoi mon code contenait autant d’instructions goto, la malfamée. Comme c’est une question qui revient souvent, je me suis dit que cela pouvait intéresser notre audience moins expérimentée en C. Here comes!

Ahem, commençons. goto, c’est un truc un peu vite fait obscur du C, un peu comme les trucs Caml obscurs de bluestorm mais quand même pas au même niveau de louchitude. goto, ça déchaîne les jeunes comme les vieux, tout ça au final pour une pauvre instruction qui sautille. Bref, goto ça transporte le flot d’exécution d’un endroit à un autre d’une fonction. Ça, c’est la description objective, notre ptit Get the Facts avant d’embrailler sur la vraie question : pourquoi diable utilise-t-on goto ? Je ne pourrai pas répondre pour « on » mais je peux répondre pour moi !

goto est avant tout l’instrument du pauvre. Voilà qui est dit. La raison principale derrière l’utilisation de goto est que celui-ci permet d’émuler des flots d’exécution atypiques et indisponibles en C. On parle souvent de gestion des erreurs, d’exceptions du pauvre ; ce n’en est qu’un cas particulier. L’idée générale est celle que je viens d’énoncer : goto permet d’écrire des programmes C avec des structures non présentes directement dans le langage.

Lol le C, c’est pour les ptits joueurs, ya pas d’exceptions

Quelques exemples maintenant, avec tout d’abord le grand classique : la sortie de boucles imbriquées et la gestion des erreurs. Pourquoi ai-je donc regroupé ces deux cas qui apparaissent distincts ? Car ils modèlent tous deux un type de flot permis par les exceptions dans les langages qui en possèdent : on dispose de contextes imbriqués desquels on veut se défaire en un saut.α

α : Notez que je n’ai pas dit que les exceptions pouvaient être émulées par des goto, juste qu’il y a des ressemblances dans le flot obtenu.

Jetons un œil à deux bouts de code. La sortie de bloc, tout d’abord :

{
    while (a) {
        while (b) {
            if (c)
                goto end;
            ...
        }
    }
end:
    ...
}

Et la gestion des erreurs, dans sa forme modelant une pile :

{
    if ((a = f(...)) == NULL)
        goto bada;
    if ((b = g(...)) == NULL)
        goto badb;
    ...
    return ...;
badb:
    antif(a);
bada:
    return ...;
}

Le premier exemple ne devrait demander aucun commentaire. Le second n’est pas aussi évident : il n’exhibe pas clairement l’idée de contextes imbriqués. Celle-ci est en vérité masquée par le court-circuitage introduit par les sauts. Nous aurions pu écrire :

{
    if ((a = f(...)) != NULL) {
        if ((b = g(...)) != NULL) {
            ...
        } else {
            antif(a);
            goto bada;
        }
    } else {
    bada:
        return ...;
    }
    return ...;
}

OK, le tout s’est sensiblement enlaidi, et je vous rassure, personne ne l’écrit ainsi (car en plus d’être laid, on effectue beaucoup plus de sauts en cas d’erreur). L’avantage de cette forme, cependant, est qu’elle modélise beaucoup plus précisément le comportement d’une exception dans un langage de plus haut niveau : chaque bloc if correspondrait à un bloc try (ou équivalent) avec une clause de finalisation (contenue dans la partie else). Lorsqu’une condition exceptionnelle se produit, les clauses de finalisation sont déroulées les unes après les autres, dans l’ordre d’empilement des contextes (représentés ici par les blocs).

À part remarquer que la gestion des erreurs, cas normal d’utilisation des exceptions, n’est pas celui qui possède la correspondance la plus naturelle avec l’utilisation de goto, nous n’avons plus grand chose à dire ici. Passons donc à…

Plus tiré par les cheveux (encore) : appels récursifs terminaux

Si vous jetez un œil aux codes sources que j’écris, il arrive que j’utilise des goto à la place de boucles classiques. Et oui, sacrilège § L’explication ? ’Tention, c’est tordu : cela modélise des appels récursifs terminaux simples.β

L’exemple typique est celui d’une fonction :

{
    T x;

re:
    if (...)
        ...
    if (...)
        ...
    if (...) {
        x = f(x, ...);
        goto re;
    }
}

Alors oui, on pourrait bien sûr écrire tout ça sous forme d’une boucle ou carrément utiliser l’appel récursif et prier pour que le compilateur C optimise.γ

C’est en vérité une question d’emphase, et ces questions là sont profondément subjectives. Une boucle mettrait moins l’accent sur l’action et davantage sur sa répétition. Dans le cas où le cas récursif est marginal, la construction s’en retrouve relativement maladroite : on a alors le choix entre une condition de boucle obscure et redondante, des marqueurs booléens dans tous les sens, ou des instructions break et continue judicieusement placées. En choisissant cette dernière solution, qui semble la moins contraignante, on se retrouve avec un code dans ce genre-là :

{
    T x;

    for (;;) {
        if (...)
            ...
        if (...)
            ...
        if (...) {
            x = f(x, ...);
            continue;
        }
        break;
    }
}

Mieux ? Je ne crois pas, mais je n’irai pas non plus cracher sur celui qui écrit cela. :) Tout cela au fond pour une question de…

β : Là aussi, soyons fous mais pas trop, on ne peut pas émuler la récursivité terminale dans le cas général avec goto.

γ : GCC le fait, m’enfin, d’autres compilateurs ne le font pas forcément, ce n’est pas une optimisation très orthodoxe en C comme elle peut l’être dans les langages fonctionnels, l’idiome étant en Cδ de tout convertir en style itératif.

δ : Mais rms a trop fait de Lisp, comme tout le monde sait. Lalala.

Lisibilité !

C’est là que les gens se fâchent, que les regards se clashent, que la violence se lâche, wash wash. Hum, les uns disent de ne pas utiliser goto, pour la clarté du code, et pour les mêmes raisons, d’autres l’utilisent ! C’est la vie. Après tout, il y bien des gens qui codent en style GNU en pensant se rendre lisibles. :)

La question de la lisibilité de goto tient principalement à sa nature la plus primaire : la possibilité de passer librement d’une portion de code à une autre. Les détracteurs de goto pointent du doigt les utilisations de l’instruction qui engendrent des transitions difficiles à suivre dans le flot. Ceux qui en défendent le gain visuel évoquent la possibilité offerte par goto de déporter des blocs de code d’un endroit à un autre. Cette astuce est principalement utilisée pour la gestion des erreurs et permet ainsi de dégager du traitement utile la gestion des cas de défaillance, rares en pratique et souvent triviaux (l’action à entreprendre étant, la plupart du temps, de faire remonter l’information telle qu’on la reçoit).

Pour conclure sur un dernier exemple, j’abuse souvent de cette propriété pour écrire des choses comme celles-ci :

{
    T *p;
    R *q;

    p = malloc(sizeof *p);
    q = malloc(sizeof *q);
    if (p == NULL || q == NULL)
        goto bad;

    ...
    return ...;

bad:
    free(q);
    free(p);
    return ...;
}

Mon style vous répugne ? N’hésitez pas à commenter. :)

Annexe : setjmp et ses copains

Si, de mon expérience, goto est généralement relativement bien reçu dans la communauté des programmeurs C dans son ensemble (excepté dans certains cercles, et dans l’enseignement, sans doute de peur de pervertir les étudiantsε), il en est autrement de setjmp, le « goto non local ».ζ

Il y a probablement plusieurs raisons à cela : la difficulté d’utilisation (cf. restrictions énoncée par la norme, dont la nécessité de déclarer toutes les variables locales manipulables après le saut volatile), mais aussi les divers problèmes lié à la conservation et la restauration des états : il faut pouvoir replacer de manière sûre le contexte au point où il était au moment de l’appel à setjmp. Cela implique la libération de mémoire et l’appel de destructeurs pour les données allouées dans l’intervalle, mais également des notions de plus bas niveau telles que la gestion des signaux (cf. sigsetjmp(3)). En plus de ça, setjmp avec toute cette complication engendre des performances moisies, en général.

setjmp est toutefois utilisé, parfois, pour implémenter des mécanismes d’exceptions, notamment dans la kazlibη (pour le C) et diverses implémentations portables de langages dynamiques (l’interpréteur interactif d’OCaml, si je me rappelle bien, et d’autres dont je ne me rappelle plus…).

ε : Il paraît que ce sont les éléments perturbateurs tels que moi qui tentent leurs petits camarades avec le côté obscur. :)

ζ : Notez que GCC possède également des goto non locaux et indirects grâce aux pointeurs d’étiquettes. Je ne m’étendrai pas à ce sujet (car je connais mal le GNU C). Plus d'informations dans le manuel de GCC.

η : Je ne vois plus dans Google la page Web correspondant. Le projet est peut-être bien décédé…

Annexe : omg l’optimisation qu’elle est bonne

On entend dire parfois que goto c’est pas bon pour les performances. Bon alors, dans l’absolu, hum, en fait il n’y a pas d’absolu. On pourrait dire que remplacer des structures de contrôle classiques par goto enlève de l’information sémantique, et bon, dans l’absolu, c’est vrai. La question est de savoir si cette information sémantique est pertinente d’une part, et si elle est utilisée, d’autre part. Question pertinence, c’est, là encore, un peu subjectif : est-ce que tel ou tel goto remplace simplement une combinaison intelligente de structures de contrôle ou ne reflète-t-il un concept absent du C ? C’est davantage une question philosophique, donc humaine, que d’importance à la machine… car en vérité, la plupart des compilateurs modernes ne tiennent absolument pas compte de ce genre de choses, et le langage intermédiaire employé n’emploie que des sauts. Ce sont ensuite les algorithmes d’analyse de flot (brrr, théorie des graphes) qui ont à leur charge de différencier les blocs et autres constructions utiles au compilateur.

[ tag:blog.huoc.org,2009:posts/29 ]
Voir les commentaires · Commenter

/\ \/

Conventions : le retour

#. Par rz0 dans Le code et ses raisons. Publié le 25.08 2009 à 14:46. Aucun commentaire.
>. Le code et ses raisons : goto en C
c débutants idiomes pratique programmation

Comme il ne coûte pas cher de déterrer des brouillons presques prêts pour en faire des articles, voici le retour de l’article sur les conventions que j’avais commencé à écrire, et duquel avait été tiré mon petit poème.

Ceci est donc un article orienté débutants. « Woah, rz0 il fait des articles de vulgarisation maintenant. » Et ouais les cocos, ce sont des choses qui arrivent, dans la vie. Parfois, en se levant le matin, et qu’il est déjà midi, on n’a pas envie de plancher tout de suite sur la refactorisation qui a échoué la veille. Bref, trève de bavardage.

Les conventions, c’est quoi ? Le principe est simple, c’est celui de la bonne ptite ouvrière : faire la même chose toujours de la même manière. Oubliez vos pulsions d’artiste et embrassez pleinement la voie de la machine (l’autre voie de la machine, pour ceux qui sont déjà en prépa) !

Et je ne parle pas ici de ne pas nommer une variable prgcnt et une autre MyFckngVisitorCount juste à côté, ça c’est la différence entre lEs GenS Ki ÉkrIv KoM sA et le monde civilisé.

Non, l’idée est que si une variable assure un certain rôle plusieurs fois, alors elle devrait avoir le même nom à chaque fois. Un peu comme les païens du C++ appellent toujours (sont toujours forcés d’appeler) le pointeur sur l’objet manipulé this. Si vous écrivez une bibliothèque de tableaux dynamiques, vous pouvez par exemple décider que n correspond toujours au nombre d’éléments du tableau actuellement considéré.

Heureusement, Le Créateur a pensé à vous, et les anciens vous ont légué une pelleté de noms prêts à l’emploi. La bonne nouvelle, c’est que vous pouvez les utiliser. La mauvaise, c’est que vous devez les utiliser. (Et c’est ici qu’intervenaient mes talents poétiques.)

Car pour la descendance, Dieu l’ordre dicta :
Pour les itérations, les compteurs i, j, k ;
n, m : des quantités ;
p, q devront pointer ;
c pour les caractères ;
s et t pour les chaînes.
Mais ni o ni l, les suppôts du binaire,
Pour que le programmeur toujours lise sans peine.

Mais il est souvent tout aussi utile de définir vos propres conventions, pour vos objets, vos types, vos règles du jeu. Ou simplement de préciser les règles traditionnelles (comme nous l’avons vu juste au-dessus avec n).

Toujours utiliser les mêmes noms pour les variables de mêmes rôles vous évitera d’avoir à commenter leur déclaration avec des banalités du genre :

int lol;      /* Nombre de cailloux dans la marre. */

Remarquez aussi, souvent, vous pourrez choisir des noms conventionnels courts. Si après avoir lu mon chant à la gloire du Grand, vous écrivez toujours indiceCaillou au lieu d’i dans votre module de gestion des cailloux dans les marres, ma foi, je ne peux rien pour vous (sinon prier pour votre âme, pour lui éviter le knout).

Un point de débat possible sur la question, cependant, est le suivant : faut-il privilégier la précision ou la convention ? (Et non, indiceCaillou ne relève pas de la précision à mes yeux.)

Imaginez, vous utilisez p pour toutes vos variables de type Meuh, en entrée des fonctions de votre module. Cela tombe bien, elles ne prennent chacun qu’un seul objet à examiner ! Parfois, en interne, vous avez besoin d’un second pointeur, que vous appelez naturellement q ; tout va bien, vous avez spécialisé les conventions existantes, parfait. Maintenant, imaginez que vous vouliez ajouter une fonction copier qui copie la valeur d’un Meuh dans un autre. Elle aura deux paramètres. Doit-on les appeler dest et src, pour la précision ? Ou p et q pour suivre la convention ? Cela nous amène au point suivant.

Tout comme il est important de se fixer des conventions pour les noms de variables, il est souvent très utile de vous définir des ordres de passage d’arguments respectés par toutes les interfaces de votre unité. Ce sont des règles du type « l’objet ayant le rôle X précède toujours cela de rôle Y ».

L’exemple typique ici est la fonction de copie. La destination doit-elle précéder ou suivre la source ? Il n’y a pas de réponse universelle, et chacun a ses préférences, mais il est important de toujours utiliser la même stratégie de classement de vos paramètres.

Pour revenir au problème précédent, s’il semble d’abord aberrant de vouloir appeler ses paramètres p et q, si l’on considère que l’ordre de passage est bien établi, alors l’usage ne fait aucun doute : p est toujours le premier pointeur et q le second, l’ordre des arguments se chargeant de donner son sens à cette position. Les deux alternatives sont donc viables ; à vous de choisir celle qui colle le plus à votre style.

Notez enfin que le point de vue est souvent influencé (un peu ou beaucoup) par le paradigme dominant. Si, disons, vous réfléchissez en orienté objet et avez codifié que le paramètre numéro un était l’objet sur lequel on opérait, alors le fait de toujours nommer cet objet de la même façon prend tout son sens. Si au contraire, vous adoptez une vision plus procédurale, un autre schéma de noms vous sera peut-être plus naturel…

Et voilà pour les conventions ! Notez que j’ai fait exprès d’éluder le sujet des idiomes, sujet fort proche. La raison ? La flemme, bien sûr. Mais pourquoi pas un jour prochain, ou un autre encore. Qui sait, tant qu’il y a du désir, et des gens pour me lire. Lalala.

[ tag:blog.huoc.org,2009:posts/25 ]
Aucun commentaire · Commenter

/\

Conventions : quelques vers à la gloire du Grand

#. Par rz0. Publié le 16.08 2009 à 14:54. 8 commentaires.
c idiomes poésie pratique programmation sectarisme

J’étais parti pour écrire une réponse au billet de GuilOooo sur BHM traitant des commentaires, mais la raison m’a rappelé à elle et j’ai décidé de vous offrir ce (très) court poème à la place :

Car pour la descendance, Dieu l’ordre dicta :
Pour les itérations, les compteurs i, j, k ;
n, m : des quantités ;
p, q devront pointer ;
c pour les caractères ;
s et t pour les chaînes.
Mais ni o ni l, les suppôts du binaire,
Pour que le programmeur toujours lise sans peine.

Et si vous avez apprécié cette haute poésie, je ne peux que vous conseiller cette belle lecture !

[ tag:blog.huoc.org,2009:posts/23 ]
Voir les commentaires · Commenter

>> Page : 0 1