Le code et ses raisons
  1. Conventions : le retour
  2. Le code et ses raisons : goto en C
  3. Le code et ses raisons : typedef en C
  4. Pourquoi le C est moins puissant que votre langage favori
  5. Préprocesseur C : récursivité ou imbrication ?
  6. Le C et ses raisons : les pointeurs restreints
  7. Le C et ses raisons : assertions ou programmation défensive ?

Le code et ses raisons : typedef en C

(Nhat Minh Lê (rz0) @ 2010-02-01 00:43:29)

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, pourquoi

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

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.