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 :
typedefs’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é
structouunion;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.
# bluestorm
02.02.10, 11:06.
struct fooetenum barsanstypedef, et pour la question deListouList *). On aurait quelque chose de concret auquel se raccrocher, et ça aèrerait un peu le texte.# Drk-Sd
02.02.10, 23:55.
Puisqu’on est encouragé à commenter même si on a pas particulièrement kiffé l’article, j’en profite. Le sujet est intéressant, du moins il m’intéressait, et j’ai pas mal accroché au début. Mais comme l’a fait remarqué bluestorm c’est très compact et j’ai pas tardé à perdre le fil, probablement par flemme… mais voilà. :/
(je suis le mec qui fait des commentaires pour dire qu’il a eu la flemme de lire, mais quel gros con me direz vous !)
Bref, je rejoins l’avis de bluestorm, faudrait aérer un peu (avec du code ?). Je reviendrais peut-être lire par la suite, mais pour le premier passage j’ai pas réussi à accrocher (un peu comme chaque fois que bluestorm poste une de ses branlettes théoriques, il faut que j’fasse plusieurs passes :D).
# gnomnain
03.02.10, 11:39.