Commentaires sur le logiciel

Généralités

Ce document traite des différents composants logiciels qui se trouvent sur ce site. Il comporte en fait des commentaires généraux de tout genre ; il n'est pas une documentation des composants, mais plutôt un recueil des diverses reflections qui m'ont passé par la tête. Du coup, certains composants sont considérés en detail, et d'autres pas du tout.

En général, je ne cherche pas à faire la concurence ni avec la bibliothèque standard, ni avec Boost. Des composants qui font double emploi avec la bibliothèque standard sont soit supprimés soit déplacer vers les répertoires des composants perimés – en tout cas, ils ne sont plus maintenus. En ce qui concerne Boost, c'est plus délicat, parce qu'un des compilateurs dont je me sers habituellement (Sun CC) ne supporte pas encore Boost, au moins dans la version que j'utilise. La règle couramment appliquée, c'est que les composants qui font double emploi avec Boost sont appelés à être supprimés à longue échéance, mais que pour l'instant, ils restent dans les répertoires courants. Ils ne subissent que de la maintenance essentielle ; des erreurs de programmation sont corrigées, mais aucune évolution se fait dans leur fonctionnalité.

Organisation des fichiers

Si on jette un coup d'oeil au répertoire de base, on constate immédiatement que le nom de certains sous-répertoires commence par une majuscule, et d'autres par une minuscule. La règle est simple : ce dont le nom commence par une minuscule ne contient que des fichiers générés, tandis que ceux dont le nom commence par une majuscule sont « permanents ».

Le code de ce site sont repartis en des répertoires suivants : Util, Exec, Benchmarks, Old et Experimental. Parmi les autres répertoires permanents, Makefiles contient – ô surprise – les fichiers de make, Tools de divers scripts de shell utilisé soit par les fichiers de make, soit pour la maintenance générale du code, et Doxygen contient tout ce qu'il faut pour générer la documetation – ou le contiendra, une fois qu'on y arrive. Le sous-systmème Old contient des composants pré-norme, tel que String. Je ne les garde que parce que j'y ai mis beaucoup de travail dans le temps, et que je n'ai pas le coeur de les supprimer. Ils ne sont plus maintenu, et je n'en parlerai plus ici.

Le répertoire Experimantal contient, comme son nom suggère, des composants où j'expérimente. En fait, il contient deux sous-systèmes : Basic, qui contient quelques composants (ou des extensions des composants présents par ailleurs) qui ne se laissent pas compiler avec tous mes compilateurs cibles. Les composants de ce sous-système sont amenés à long terme à migrer vers Util. L'autre sous-système, ISO10646 contient les débuts d'un support des caractères Unicode (sur des entiers 32 bits). J'y ai travaillé beaucoup entre mars et juin 2002, entre deux contrats. Depuis, je n'y ai à peine touché ; d'une part, il me manque du temps, et de l'autre, je ne suis pas tout à fait convaincu que mon approche en était le bonne. Au moins pour le genre de chose que je fais, je crois de plus en plus que la solution est l'utilisation du UTF-8 (et non USC-4), même à l'intérieur du programme. Je ne traite donc pas ce répertoire non plus en ce qui suit.

Parmi les trois autres répertoires, deux, Exec et Benchmarks, contiennent des programmes entiers, exécutable, tandis qu'Util contient la bibliothèque générale (ce qui répresente de loin la plus grande partie du code ici). Le répertoire Benchmarks contient quelque benchmarks que j'ai écrit, pour la plupart en réaction à un article dans un forum. Pour la plupart, je crois que leur intérêt est assez limité, avec l'exception de Hashcode, qui compare les performances de différents algorithmes d'hashage. Une évaluation des résultats de Hashcode est d'ailleurs documentés dans Hash Code Evaluations (en anglais).

Comme pour la reste, l'intérêt des programmes dans Exec varie. Certains, comme comment, aligneq ou macroize me servent prèsque quotidiennement. D'autres, comme enumgen, sont d'un intérêt certain dans certains cas. D'autres ne sont pratiquement que pour s'ammuser ou pour démonstrer l'utilisation de la bibliothèque. Pour chaque programme que j'ai pensé puisse avoir un intérêt a une page man propre (en anglais) ; en se positionnant dans le répertoire Exec et en exécutant « make doc », voir Index. (Le format et le style de ces pages de man sont modélés sur ce de The Single UNIX Specification.) Je n'en parlerai donc plus ici non plus.

Reste les sous-systèmes sous Util (connues collectivement sous le nom GABIlib), qui seront la thème de la reste de ce document.

Sous-système Port

Ce sous-système contient tout ce que concerne la portabilité ou qui dépend du système ou du compilateur. En fait, le composant Global contient l'en-tête gb/Global.hh, qui doit toujours être le premier inclu ; en plus des aspects qui concerne la portabilité, il définit la version, et une ou deux autres choses qui servent à peu près partout.

Il n'y a qu'un module ici qui mérite vraiment un commentaire : StackTrace. C'est certainement celui qui posera le plus de problème lors d'un portage (au moins de se contenter d'une implémentation factice qui renvoie toujours null), et c'est la raison pour laquelle je n'ai même pas essayé d'utiliser des outils comme autoconf. Actuellement, les seules machines auxquelles j'ai accès, ce sont des PC's (Intel 32 bits) et des Sparcs ; c'est donc les seules machines pour lesquelles je peux en fournir des versions ici.

Sous-système Test

C'est à mon avis l'élément le plus outil de tout ce qui s'y trouve. Dans ce sous-système est défini le harnais de tests, l'engin de leur exécution, la vérifiction de la mémoire (pour ceux dont le budget ne permet pas Purify®) ainsi que des classes du support des tests et des benchmarks.

MemoryCheck n'a pas encore été mis à jour pour reflechir le fait que je travaille de nos jours surtout avec GC. Même avec garbage collection, il a un intérêt, puisqu'il détecte des débordements de la zone allouée, et qu'il initialise le block avec un motif prédéterminé (0xDEADBEEF) qui d'une part ne fonctionnera probablement pas comme pointeur ou du texte si on oublie une initialisation, et qui de l'autre est facilement identifiable.

Pour TestSequence, voir aussi Tools/makemain.

Sous-système Basic

Des composants AssocArrayOf et HashTableImpl seront amenés à disparaître (ou de passer à Old) dès qu'on a réelement std::unordered_map et std::unordered_set

Les composants RefCntPtr et ManagedPtr ne servent pour ainsi dire plus, depuis que j'utilise le glaneur de cellules de Boehm. Sans le glaneur de cellules, la fonctionalité de RefCntPtr est subsumée par Boost::shared_ptr. ManagedPtr pourrait être intéressant dans certains cas  il a l'avantage par rapport à Boost::weak_ptr d'être indépendant du comptage des références. Mais mon expérence depuis que je l'ai, c'est que c'est rarissime que la mise à nul du pointeur suffit comme notification ; il faut prèsque toujours une vraie notification.

Je n'ai pas encore eu l'occasion de vérifier, mais c'est possible que les composants FixedLengthAllocator et IOSave fassent double emploi avec des composants de Boost. De toute façon, il sont tous les deux d'anciens composants qui n'ont pas subi de modifications depuis des années. La question du niveau de maintenance auquel ils sont soumis ne se présente donc pas. La même chose vaut pour Random, mais la dernière fois que j'ai régarder, l'implémentation dans Boost comportait encore des erreurs.

On pourrait arguer que BitVector et DynBitVector (et donc BitVectorImpl, auquel ils se basent) ont des equivalents dans la norme. Mon impression est que l'interface ici est beaucoup plus complète et cohérente ; je trouve aussi intéressant que la version à taille statique et celle dynamique aient la même interface. (L'interface de std::bitset, si elle n'est pas aussi riche que celle de BitVector, me semble quand même assez utilisable. Mais prétendre que std::vector convient pour la même chose, mais avec une longueur dynamique, c'est prèsque se moquer du monde.)

À l'autre extrème, Array et Fallible sont tellement utiles que je m'étonne de ne pas les trouver ailleurs. Je crois que je n'ai jamais écrit une application sans utiliser Fallible, et la nécessité d'utiliser les tableaux de type C, soit pour interfacer avec des logiciels anciens, soit pour des raisons de l'ordre d'initialisation, fait que Array sert aussi beaucoup. (On pourrait, par exemple, noter son utilisation dans le code généré par enumgen.)

Sous-système Text

Tous les composants ici ne traitent que du char. Il n'y a pas de support pour les wchar_t, ni pour les caractères composés (« multi-byte characters » en anglais). J'avais commencé une implémentation pour des caractères Unicode (UCS-4) dans le répertoire Experimental, mais je n'ai pas eu le temps de le mener au bout. En plus, je suis de plus en plus convaincu que pour mes besoins, l'UTF-8 dans des char convient plus. (J'ai effectué quelques expériments dans cette direction, mais rien qui ne peut être diffuser.) Ces composants ne sont donc utile que si la « internationalisation » se limite à l'Europe de ouest et les Amériques.

D'un certain côté, le composant RegularExpression fait double emploi avec boost::regex. Dans ce cas-ci, en revanche, je crois que je garderai le composant même après l'adoptation de Boost ici. Il a fait d'autres choix que Boost dans plusieurs aspects, et donc garde sont utilité. En gros : Boost supporte toutes les extensions aux expressions rationnelles, comme les sous-séquences. Du coup, Boost est beaucoup plus souple, et doit normalement recevoir la préférence. En revanche, l'absence du support des sous-séquences permet deux chose qui ne sont pas disponisble chez Boost : l'implémentation ici utilise un automat fini défini (évalué normalement de façon paresseuse), ce qui doit le rendre beaucoup plus rapide sur des quantités de données importantes, et surtout, elle a des fonctions supplémentaires pour provoquer l'évaluation complète de l'automat et d'en extraire les tableaux, ce qui peut être très intéressant dans le cas où l'expression rationnelle est fixe : on évite de l'évaluer complètement à l'exécution. (En fait, la classe ici a été conçue originalement avec l'idée d'implémenter un outil du genre lex.)

StringUtils est surtout présent pour facilité la migration de ma classe String d'avant la norme. Mais je crois que les fonctions qu'il contient ne sont pas sans un intérêt général.

Pour la reste, FieldArray, SetOfCharacter et CharacterClass me servent assez régulièrement. ParserSource est un essai simplement de donner une implémentation plus légère à la deuxième forme des itérateurs dans C++ : les streambuf. En fait, je constate que quand on parse du texte, on a la plus souvent besoin des itérateurs très simple, mais avec sémantique de référence, et qui supporte le polymorphisme dynamiques. (Un parseur devient bien vite trop complexe pour vouloir l'implémenter comme un template. Dans l'absence du support pour export, en tout cas. Moi, au moins, je réchigne à mettre des dizaines de milliers de lignes dans un en-tête.)

Sous-système IO

Je ne suis pas sûr que les composants CRC et Digest doivent réelement se trouver dans le sous-système IO, mais pour l'instant, je n'ai pas d'autre endroit qui conviendrait plus. On pouvait arguer pour Text dans le cas de Digest. Mais le hachage se fait sur n'importe quelle suite d'octets, non forcément des caractères, et son ressemblance à CRC fait que je n'ai pas envie de les séparer.

Les composants FFmt, EFmt et HexFmt peuvent servir tels quels. À mon avis, cependant, ils sont beaucoup plus intéressants en tant qu'exemples de comment on crée un manipulateur sur mesure, afin de créer des balises logiques, et non physiques. C-à-d qu'on ne spécifie pas les caractèristiques logiques du formattage, comme la précision ou le format exact, mais le caractèristique logique : c'est de l'argent, un pourcentage, etc. Et qu'on a des manipulateurs qui s'appellent « currency », « percent », etc.

Les composants FilteringInputStream et FilteringOutputStream font double emploi avec des classes dans boost::iostreams, seront supprimés dès que tous mes compilateurs supportent Boost. Les versions dans Boost sont à tout égard supérieur à ce qui se trouve ici : plus propre (c'est l'avantage d'être deuxième:-)), et avec beaucoup plus de fonctionalité. Dès maintenant, je dirais de n'utiliser les implémentations ici que si, comme moi, tu ne peux pas utiliser Boost.

Sur Format en particulier

On pourrait dire aussi que Format fasse double emploi avec Boost. Seulement, l'implémentation Boost utilise actuellement l'opérateur '%' pour l'insertion, ce qui le rend quasiment inutilisable : les expressions qui en résultent sont illisible. Si on tient à abuser le surcharge des opérateurs comme ça, il faut un opérateur qui a à la fois un poids graphique important et une précédance assez basse – le « << » utilisé par les iostream est déjà à la limite en ce qui concerne la précédence. Mes préférences vont à une fonction avec nom, par exemple « with() » (comme dans mon implémentation). Apparemment, beaucoup considère, en revanche, que c'est trop lourd. Andrei Alexandrescu (dans un article dans le C/C++ Users Journal, août, 2005, disponible à l'Intel Developer PipeLine) propose l'utilisation de l'opérateur () comme compromis (qu'il appelle le « Trailing Parens Idiom ») ; à vrai dire, je préfère encore la fonction avec nom, mais comme compromis, je trouve sa suggestion acceptable.

Il y a aussi des différences importantes dans la fonctionalité de l'implémentation Boost et celle ici. L'implémentation ici supporte toutes les spécifications de formattage de C90, avec en plus l'extension Open Systems® pour les paramètres positionnels. Boost ne supporte pas les modificateurs les plus exotiques ; en revanche, il permet l'utilisation des manipulateurs, ce qui ne marche pas avec l'implémentation ici. Dans ce cas-ci, je crois que c'est Boost qui a raison. Je n'ai implémenté toutes les options qu'en réponse à un défi, et non parce que je croyais qu'elles soient essentielles. Plus important : je crois que la présence des spécifications de formattage dans la chaîne est une erreur. Le choix d'un format dépend du type et de la sémantique de la variable qu'on formatte, et non de la langue d'affichage. Il doit donc se trouver dans le code, et non dans la chaîne d'affichage.

Ce qui me mène au point final : le formattage à la printf est une solution fondamentalement mauvaise. Ça me fait de la peine à le reconnaître. J'ai travaillé des années en C ; je le connais, et je me sens comfortable avec elle. Mais ce qui semblait même une bonne solution en 1980 ne l'est plus aujourd'hui. (Et même en 1980, je dirais que le PRINT USING de la plupart des Basic était supérieur.)

En plus, avec la tendance à utiliser des messages de plus en plus proche au langage humain, les paramètres positionnels ne suffisent plus à l'internationalisation. Pense à un format comme (en anglais) « "The %d %s %s" », ou le %d donne le nombre, le premier %s un adjectif (disons le couleur) et le deuxième le nom. Déjà en anglais, on a un problème avec le nom, et il faut passer quelque chose du genre « names[ count == 1 ][ objId ] » comme paramètre, où names[0] donne les noms singuliers, et names[1] les noms pluriels. Pour le traduire en français, il faut bien plus que simplement le changer en « "Le %1$d %3$s %2$s" », si vous lisez ces lignes, vous savez bien qu'il faudrait aussi :

On finit donc avec quelque chose du genre :

        Gabi::Format( "%s %d %s %s" )
            .with( articles[ count == 1 ][ name[ id ].gender ] )
            .with( count )
            .with( colors[ count == 1 ][ name[ id ].gender ][ colorId ])
            .with( name[ id ].text[ count == 1 ] ) ;
    

Au moins, on n'a rien à changer quand on passe en anglais, bien que beaucoup des tableaux y seront identique. Ça marche même pour l'italien, l'espagnol et un certain nombre d'autres langues de l'Europe occidentales. En allemand, il faudrait ajouter le fait que les noms, les adjectifs et les articles varient aussi selon le cas : « die Kinder » (les enfants) devient « den Kindern » après la préposition « mit » (avec), par exemple.

Et on n'a pas encore quitter l'Europe occidentale. Dans le monde, il y a beaucoup de variété dans la façon que fonctionne les langues. Certaines langues, par exemple, ont un duel, c-à-d qu'il faudrait traiter le cas où count == 2 à part aussi. (J'avais déjà laisser de côté le cas de 0 en français, pour simplifier.) De prèsque j'ai entendu dire, la russe utilise le singulier après des nombres comme 21, qui se termine par le vocable « un » (et comme en français, ce n'est pas le cas d'onze). Et que penser des langues agglutinantes, comme l'hongrois ou le finnois, ou des langues à classe tellement répandues en Afrique. Je ne connais pas de solution simple, mais j'imagine que toute solution passe par des objets dynamiques (les DLL de Windows ou les .so des Unix), avec un objet précis par langue.

En attendant, je dirais que si l'internationalisation n'est pas une thème, les iostream, avec les manipulateurs bien conçus, reste la meilleur solution. Sinon, si on ne fait que des choses ultra-simple, qu'on peut s'en tirer avec simplement des paramètres positionnels, et qu'on peut supporter l'utilisation de l'opérateur % (ce qui implique au minimum une coloration par syntaxe pour régarder le code, toujours), boost::format, toujours avec des manipulateurs bien conçus, pourrait faire l'affaire – avec des spécificateurs comme « "%n%" », on peut se passer de l'apprentissage de toute les subtilités du formattage à la C.