Code de grande personne – Affichage selon une base de numération

Ce texte repose sur quelques techniques peu connues de la plupart des gens au moment d'écrire ces lignes. Avant de procéder, je vous recommande de lire et de comprendre cette brève introduction à la métaprogrammation.

Un chic étudiant de la cohorte 03 du Diplôme de génie logiciel de l'Université de Sherbrooke, Louis Robichaud, m'a soumis l'autre soir un petit problème mathématique amusant qui m'a mené vers un problème de programmation en soi fort divertissant. Louis me pardonnera, je l'espère, de ne pas me souvenir du lieu où il avait pigé ce problème à l'origine (en fait, il m'a indiqué les sources – ici – au mieux de ses souvenirs mais n'y jetez pas un coup d'oeil trop rapidement puisque son message à ce sujet divulgue la clé de la solution).

Louis m'a proposé ce problème alors que je comptabilisais le temps restant pour un examen. J'ai pris l'habitude, avec les années, d'écrire le temps restant selon plusieurs systèmes de numération (décimale, octale, hexadécimale, binaire, romaine, etc.).

L'énoncé se posait à peu près comme suit :

Sachant tout cela, quelle est la base x? Indice : il s'agit d'une base entière et monotone.

Ce problème a plusieurs bons côtés, en ce qu'il force à réfléchir hors des sentiers battus, hors du cadre, un peu comme le préconise la pensée latérale (voir Exprimer... autrement). La réponse à la question de Louis (et une représentation programmée de ce que cela nous offre comme possibilité) est disponible plus bas.

Avant de répondre à la question de Louis, conceptuellement et par programmation, nous explorerons la question de l'affichage selon plusieurs bases. Ceci nous permettra de mêler métaprogrammation, traits et plusieurs autres techniques amusantes.

Le programme de test

Commençons, de manière raisonnable, par définir ce dont aura l'air notre programme de test. Notre objectif sera de mettre en place un mécanisme capable d'afficher une valeur entière selon une base de numération entière et positive choisie.

Les bases qui nous intéresseront se situeront entre 2 et 36 inclusivement, du fait que la base 1 est un cas dégénéré et peu intéressant et du fait que la base 36 peut être exprimée par des unités de valeur 0-9a-z (on pourrait aller plus loin mais la question deviendrait de choisir des symboles pour afficher chaque unité; on n'y gagnerait pas conceptuellement).

Le programme suivant montre quelques exemples de ce que nous pourrions vouloir exprimer et nous permettra de précisér nos attentes.

#include "as_base.h"
#include <iostream>
int main() {
   using namespace std;
   wcout << afficher_base<3>(5, L"En base trois: ") << endl;
   cout  << afficher_base<10>(14) << endl
         << afficher_base<10>(10, "En base 10: ") << endl
         << afficher_base<16>(14) << endl
         << afficher_base<8>(15) << endl;
}

Ce code affichera le texte que vous pouvez voir à droite. La valeur 12 (au sens décimal : 1*3+2*1) en base 3 représente effectivement la même valeur que 5 en base 10. De même, 14 décimal et 0xe hexadécimal sont deux écritures pour la même valeur.

En base trois: 12
14
En base 10: 10
0xe
017

Les textes (optionnels) suppléés en tant que second paramètre à afficher_base() sont des préfixes pouvant être utilisés lors de l'affichage. Par défaut, nous voudrons respecter les conventions C++ de représentation (préfixe 0 pour les valeurs exprimées sous forme octale, comme 017 qui est l'équivalent octal de 15 exprimé sous forme décimale, préfixe 0x pour les valeurs hexadécimales, et aucun préfixe par défaut pour les autrs représentations). Ce choix se veut à la fois convivial et simple : par défaut, l'affichage respectera les usages, mais il sera possible de diverger des usages si nous le jugeons pertinent.

Remarquez aussi le choix, délibéré, de mettre en place du code supportant à la fois les flux de caractères étendus comme std::wcout et les flux de caractères simples comme std::cout. Ceci aura un impact sur notre implémentation, qui devra être raffinée sans perte d'efficacité. Le gain en souplesse à l'usage, manifestement, en vaudra la peine.

Quelques outils généraux

Tout d'abord, mettons en place quelques outils généraux, qui nous permettront de mieux construire le reste de notre stratégie par la suite.

Nous aurons recours à des assertions statiques pour valider à la compilation certaines conditions essentielles de notre démarche. Par exemple, l'exigence posée plus haut à l'effet que les bases de numération usuelles soient dans un intervalle défini (2..36) pourrait être validée à l'exécution mais ce serait de gaspillage de ressources étant donné que, dans le modèle que nous échafaudons ici (voir le programme de test proposé), les bases seront connues à la compilation – si les bases devaient être choisies à l'exécution, la situation serait toute autre.

#ifndef STATIC_INFO_H
#define STATIC_INFO_H
#include <type_traits>
template <auto N>
   struct strictement_positif : std::bool_constant<(N > 0)> {
   };
template <auto N>
   constexpr bool strictement_positif_v = strictement_positif<N>::value;
template <auto N>
   struct positif : std::bool_constant<(N >= 0)> {
   };
template <auto N>
   constexpr bool positif_v = positif<N>::value;
#endif

Les descriptifs statiques strictement_positif<N>::value et positif<N>::value ne sont pas du tout essentiels à notre propos mais clarifieront à certains égards l'intention dans le code. Ils ne coûtent absolument rien à l'exécution (ce sont, après tout, des constantes).

Considérations alphabétiques

Nous serons préoccupés par l'alphabet pour au moins deux raisons :

Nous aurons donc recours à des traits pour décrire les éléments d'un alphabet occidental conventionnel (A-Z) sur un type de caractère donné.

Notez que j'ai choisi d'intégrer les traits standards sur les caractères (std::char_traits), utilisés entre autres dans les flux standards, aux traits d'un alphabet.

Les traits d'alphabet proposés ici sont simplistes et pourraient certainement être enrichis par beaucoup d'information pertinente supplémentaire. Je m'en suis simplement tenu à ce dont j'avais besoin dans l'immédiat.

#ifndef ALPHABET_TRAITS_H
#define ALPHABET_TRAITS_H
#include <string>
template <class C = char>
   struct alphabet_traits {
      using traits = std::char_traits<C>;
      using str_type = std::basic_string<C>;
      using char_type = typename traits::char_type;
      static constexpr char_type
         MIN = static_cast<char_type>('A'),
         MAX = static_cast<char_type>('Z');
      static constexpr typename
         str_type::size_type TAILLE = MAX - MIN + 1;
   };
#endif

Outils de base pour la gestion des bases

Pour faciliter le travail propre à la gestion des bases et des préfixes lors de l'affichage, nous mettrons en place un ensemble de petits outils et de petits traits utiles.

Le premier outil, la fonction chaine_vers_chaine(), prendra en paramètre une instance d'une chaîne standard sur un type de caractères et retournera une instance d'une chaîne standard sur un autre type de caractères.

#ifndef BASE_BASES_H
#define BASE_BASES_H
#include <string>
#include <algorithm>
#include <iterator>
template <class D, class S>
   std::basic_string<D>
      chaine_vers_chaine(const std::basic_string<S> &src) {
      using namespace std;
      return { begin(src),end(src) };
   }

Notez que l'implémentation proposée ici est légèrement incorrecte et mériterait d'être raffinée, en particulier en lien avec l'internationalisation du code, mais dans ce petit programme – qui n'utilise en réalité que des caractères alphabétiques occidentaux – cette implémentation suffira.

Viennent ensuite les traits des bases de numération.

Le trait de base pour une base N donnée aura pour préfixe une chaîne de caractères vide, donc aucun préfixe, comme le veut l'usage. Notez que l'on parle ici d'une chaîne vide de caractères d'un type déterminé en fonction des besoins.

template <int N, class C = char>
   struct base_traits {
      static std::basic_string<C> prefixe()
         { return {}; }
   };

Nous avons recours à une méthode de classe encapsulant une constante locale pour déterminer le préfixe à utiliser pour une base donnée du fait que la constante n'est pas entière, ce qui alourdirait la syntaxe de sa définition si nous voulions en faire une constante de classe. L'écriture utilisée ici est à la fois simple et efficace dans les circonstances.

Nous spécialiserons ensuite les traits de bases de numération pour les bases pour lesquelles un préfixe usité est souhaité :

  • La base binaire avec préfixe 0b
  • La base octale avec préfixe 0
  • La base hexadécimale avec préfixe 0x.

Remarquez le recours à la fonction chaine_vers_chaine() pour alléger l'écriture des expressions. 

On remarquera ici la spécialisation d'un template général (base_traits<N,CType>) sur la base de diverses valeurs entières pour N plutôt que sur la base de divers types de données.

template <class C>
   struct base_traits<2, C> {
      static auto prefixe() {
         using std::literals;
         return chaine_vers_chaine<C>("0b"s);
      }
   };
template <class C>
   struct base_traits<8, C> {
      static auto prefixe() {
         using std::literals;
         return chaine_vers_chaine<C>("0"s);
      }
   };
template <class C>
   struct base_traits<16, C> {
      static auto prefixe() {
         using std::literals;
         return chaine_vers_chaine<C>("0x"s);
      }
   };

Enfin, nous définirons deux classes vides qui nous permettront, éventuellement, de choisir entre deux stratégies d'affichage distinctes.

Utiliser des types pour aiguiller de manière statique le choix d'un algorithme est une tactique répandue dans la bibliothèque standard (le cas patent est celui de la fonction std::distance()).

class recursif_queue {};
class iteratif_tete {};
#endif

Pour l'aiguillage statique des algorithmes, retenez simplement que nous expliquerons la manoeuvre en temps en lieu.

Afficher selon une une base de numération régulière – une politique

Pour avoir un système d'affichage souple (et couvrir, éventuellement, le cas de la base x qui nous a amené à échafauder ce système), nous définirons la stratégie d'affichage en deux temps :

Le type base_reguliere<N> définira les politiques d'une base de numération dite régulière (décimale, octale, hexadécimale, base 3, peu importe). Le découpage des politiques se fera comme suit :

  • Le prédicat est_signifiante(n) sera vrai si et seulement si n est non nul. L'algorithme récursif par la queue d'affichage que nous examinerons plus bas utilisera cette politique comme condition d'arrêt. Sa particularité sera d'afficher le nombre le plus à gauche d'abord (puisque les humains lisent de gauche à droite) et la condition d'arrêt définie par est_signifiante(n) permettra d'interrompre le calcul lorsque le seuil des zéros non significatifs (les zéros à gauche) aura été atteint
  • La méthode prochaine(n) retournera la prochaine valeur à considérer dans l'algorithme d'affichage alors que la méthode courante(n) retournera la valeur à afficher pour une valeur n donnée. Afficher un nombre entier en base N implique habituellement une combinaison de divisions entières et de restes de divisions entières par N, ce qui est reflété par notre politique
  • Pour fins de convivialité, nous aurons un constructeur par défaut et un constructeur paramétrique sur un entier, les deux fusionnés en un seul. Une instance de base_reguliere<N> étant sans état (sans attributs d'instance), ce constructeur n'aura d'autre raison d'être que de faciliter l'intégration générique avec d'autres politiques (plus bas).
#ifndef BASE_REGULIERE_H
#define BASE_REGULIERE_H
#include "base_bases.h"
template <int N>
   struct base_reguliere {
      using prod_categ_t = recursif_queue;
      static constexpr bool est_signifiante(int n) {
         return prochaine(n) != 0;
      }
      static constexpr int prochaine(int n) {
         return n / N;
      }
      static constexpr int courante(int n) {
         return n % N;
      }
      constexpr base_reguliere(int = {}) {
      }
   };
#endif

Ayant défini la politique qui servira à guider la mécanique d'affichage, nous définirons maintenant la stratégie d'affichage en tant que telle.

L'implémentation de l'affichage d'un nombre selon une base de numération N et une politique OpBase données ira comme suit. Une version légèrement plus raffinée sera proposée plus bas. La base de notre démarche sera de représenter l'affichage à l'aide d'un foncteur générique nommé as_base_impl (pour implémentation).

Utiliser un foncteur aura ici plusieurs avantages, incluant bien sûr la gestion des états du traitement et la définition de types et de constantes internes.

Notez la syntaxe de la déclaration de notre politique d'affichage : il doit s'agir d'une entité générique sur la base d'un int.

Par défaut, les caractères traditionnels et une base régulière seront présumés (nous présumerons qu'il s'agit là du cas type). Le code client pourra, au besoin, faire d'autres choix. Évidemment, le code client devra spécifier la base de numération à utiliser pour l'affichage.

#ifndef as_base_H
#define as_base_H
#include "base_reguliere.h"
#include "alphabet_traits.h"
#include <iosfwd>
#include <string>
template <
   int N, class C = char,
   template<int> class OpBase = base_reguliere
>
class as_base_impl {

Pour alléger la syntaxe de notre foncteur, nous y définirons deux types internes que nous nommerons oper_t (la politique appliqués à l'entier N choisi par le code client) et char_type (le type de caractère à utiliser pour l'affichage).

   using oper_t = OpBase<N>;
   using char_type = C;
   using str_type = std::basic_string<
      char_type
   >;
   using ostream_type = std::basic_ostream <
      char_type,
      std::char_traits<char_type>
   >;

Le foncteur aura recours à quatre attributs d'instances :

  • Le préfixe à utiliser pour l'affichage – nous devons le prendre en note puisqu'il peut être suppléé par le code client
  • Le flux sur lequel afficher (notez la définition complexe, qui tient compte du type de caractère à utiliser)
  • La politique d'affichage, et
  • La valeur à afficher.

La complexité associée à la prise en charge de plusieurs types de caractères alourdit (syntaxiquement) le code serveur mais allège le code client. C'est donc une sage décision de design.

   str_type prefixe;
   ostream_type &os;
   oper_t oper;
   int val;

Pour alléger le code, quelques constantes de classes sont définies. Notez que TAILLE_ALPHABET est bel et bien une constante de classe puisque char_type est défini pour le type entier et non pas pour une instance à la fois.

   static constexpr auto TAILLE_ALPHABET =
      alphabet_traits<char_type>::TAILLE;
   static constexpr int NB_UNITES_CHIFFRES =
      '9' - '0' + 1;

Notre exigence quant aux valeurs acceptables des bases entières est définie par une assertion statique, comme il se doit.

   static_assert(
      strictement_positif_v<N> &&
      (N < (NB_UNITES_CHIFFRES + TAILLE_ALPHABET)),
      "base invalide"
   );

Une fonction pour obtenir le symbole correspondant à une unité permet d'alléger le code dans son ensemble.

La méthode de classe symbole() joue ce rôle. La validation est inutile considérant les assertions statiques (si les politiques menant à la génération des unités sont correctement codées).

Notez que cette méthode, bien que verbeuse, contient deux constantes locales déterminées à la compilation et une conditionnelle fortement optimisable. Son coût est microbien.

   static constexpr char_type symbole(int n) {
      static constexpr char_type BASE_CHIFFRES('0');
      static constexpr char_type BASE_LETTRES('a');
      return (n < NB_UNITES_CHIFFRES)?
         static_cast<char_type>(n + BASE_CHIFFRES) :
         static_cast<char_type>(
            BASE_LETTRES + n - NB_UNITES_CHIFFRES
         );
   }

La code réalisant l'affichage pour une base donnée et une politique donnée est donné par la méthode d'instance affichage_brut(). Remarquez que la stratégie d'affichage est récursive par la queue (elle évalue les unités à afficher du plus petit au plus grand mais affiche du plus grand au plus petit).

Une alternative itérative procédant par la tête est aussi offerte, mais l'affichage sera alors différent.

   void affichage_brut(int n, recursif_queue prod) {
      if (oper.est_signifiante(n))
         affichage_brut(oper.prochaine(n), prod);
      os << symbole(oper.courante(n));
   }
   void affichage_brut(int n, iteratif_tete) {
      os << symbole(oper.courante(n));
      n = oper.prochaine(n);
      while (oper.est_signifiante(n)) {
         os << symbole(oper.courante(n));
         n = oper.prochaine(n);
      }
   }

Le constructeur est simple et ne fait que noter, dans des attributs d'instances, les données pertinentes à l'affichage (valeur, flux en sortie et politique d'affichage).

public:
   as_base_impl(int n,
       ostream_type &os,
       const str_type &prefixe =
          base_traits<N>::prefixe(),
       oper_t oper = {})
      : val{ n }, os{ os }, prefixe{ prefixe },
        oper{ OpBase<N>(n) } {
   }

Enfin, l'affichage est, en soi, banal. Une valeur nulle produira une sortie de 0 sans préfixe particulier (ceci aurait pu être un trait de base_traits<N>) alors que toute autre valeur produira une sortie gérée par la politique choisie et précédée du préfixée déterminé à la construction du foncteur.

   void operator()() {
      if (val) {
         os << prefixe;
         affichage_brut(val);
      } else {
         os << 0;
      }
   }
};

Il est possible d'alléger la syntaxe des programmes lors de recours à une instance de as_base_impl en utilisant des structutres intermédiaires faciles à optimiser pour un compilateur.

Une manière simple de réaliser une telle optimisation est d'utiliser un autre foncteur, nommé ici as_base, et qui déduirait une partie des paramètres génériques à partir des paramètres de son constructeur et à partir des paramètres de son opérateur ().

Le truc est simple : invoquer une fonction (incluant l'opérateur () d'un objet) permet au compilateur de déduire le type de certains opérandes, ouvrant la porte à une simplification syntaxique du code client.

template <
   int N,
   template <int> class OpBase = base_reguliere,
   class C = char
>  class as_base {
   public:
      using str_type = std::basic_string<C>;
      using char_type = C;
      using ostream_type = std::basic_ostream <
         char_type,
         std::char_traits<char_type>
      >;
   private:
      int val;
      str_type prefixe;
      OpBase<N> oper;
   public:
      as_base
         (int n, OpBase<N> oper = {},
          const str_type &prefixe =
             base_traits<N, char_type>::prefixe())
         : val{ n }, oper{ oper }, prefixe{ prefixe } {
      }
      ostream_type& operator()(ostream_type &os) const {
         as_base_impl<
            N, char_type, OpBase
         > aff (val, os, prefixe, oper);
         aff();
         return os;
      }
   };

À strictement parler, on peut aller encore plus loin dans la simplification en utilisant des fonctions. Pas aussi loin qu'on ne le voudrait parfois (nous le verrons plus loin), mais plus loin tout de même.

Une première « simplification », évidente sans doute, est d'offrir une version servant de relais au constructeur de as_base.

template <int N>
   as_base<N> afficher_base(int n) {
      return { n };
   }

Une deuxième simplification serait de déduire le type de caractère à utiliser du type du préfixe suppléé par le code client.

template <int N, class C>
   as_base<N, base_reguliere, C>
      afficher_base (int n, const std::basic_string<C>&) {
      return { n };
   }

Deux déclinaisons de cette simplification sont raisonnables : une qui utilise un const C* brut et une autre qui utilise une std::basic_string<C>.

template <int N, class C>
   as_base<N, base_reguliere, C>
      afficher_base(int n, const C *prefixe) {
      return { val, base_reguliere<N>(), prefixe };
   }

Enfin, pour faciliter l'enchaînement des affichages sur un flux, un manipulateur de flux spécialisé pour nos objets peut être offert.

template <int N, template <int> class OpBase, class C>
   auto& operator<<(std::basic_ostream<C> &os,
                    const as_base<N, OpBase, C> &aff) {
      return aff(os);
   }
#endif

Voilà. Nous avons un afficheur opérationnel en fonction de bases traditionelles. Attaquons maintenant le problème de la base x.

Ouvrir les portes de l'esprit

Dixit Louis : « Un article sur l'utilisation de la base factorielle est apparue dans CACM (Communications of the ACM), ou possiblement dans ACM Sigplan Notices (ACM SIG on Programming Languages) il y a plus de dix ans. On examinait ses propriétés : nombres rationnels avec une représentation acyclique, neutralité de la base vis-à-vis les une valeur particulière. J'aurais de la difficulté à le retrouver, à cause du manque d'index général. ».

La base x est une base factorielle : l'entier 123 en base x a la valeur 1*3! + 2*2! + 3*1!, donc (pris d'un point de vue décimal) 6 + 4 + 3 ou 13. Notez que la base la plus à gauche est un multiple de 1! plutôt que de 0! car 0!==1!. Une caractéristique de cette base est que la valeur maximale des unités croît de manière monotone de la droite vers la gauche (ce qui, quand on y pense, est très raisonnable).

Pour reprendre l'un des exemples initiaux, un individu ayant 52 ans avec une écriture décimale aura 2020 ans en base x, ce qui équivaut à 2 * 4! + 2 * 2! ou 2 * 24 +2 * 2 avec une écriture décimale.

Sachant cela, quelques remarques s'imposent :

Voyons voir où ça nous mène...

Le programme de test

Modifions notre programme de test pour voir où nous souhaitons aller sur le plan de l'écriture du code client. Remarquez que les invocations spécifiant une base autre que base_reguliere utilisent as_base plutôt que afficher_base pour des raisons historiques (on peut faire mieux).

#include "base_factorielle.h"
#include "as_base.h"
#include <iostream>
int main()
{
   using namespace std;
   wcout << afficher_base<3>(5, L"En base trois: ") << endl;
   cout  << afficher_base<10>(14) << endl
         << afficher_base<10>(10, "En base 10: ") << endl
         << afficher_base<16>(14) << endl
         << as_base<10, base_factorielle>(22) << endl
         << as_base<10, base_factorielle>(17) << endl
         << as_base<10, base_factorielle>(6)  << endl
         << as_base<10, base_factorielle>(35) << endl
         << as_base<10, base_factorielle>(52) << endl;
   cout << afficher_base<8>(15) << endl;
}

De quoi aurait l'air la politique base_factorielle? Voici quelques présomptions pour guider notre réflexion :

Voyons voir comment il serait possible d'y arriver.

La première étape est d'être en mesure d'évaluer toute factorielle aussi tôt que possible (ici : à la compilation, évaluer Facto<N>::value pour tout N pertinent).

La technique est classique et est décrite dans l'article sur la métaprogrammation.

#ifndef BASE_FACTORIELLE_H
#define BASE_FACTORIELLE_H
#include "base_bases.h"
#include "static_info.h"
template <auto N>
   struct Facto {
      enum { value = N * Facto<N-1>::value };
   };
template <>
   struct Facto<0> {
      enum { value = 1 };
   };

Pour obtenir efficacement la factorielle d'un N obtenu dynamiquement, une solution simple est de calculer a priori tous les cas possibles (ici, le nombre de cas possibles est restreint).

L'initialisation du tableau des valeurs possibles pour la factorielle d'un entier sera réalisée avant de le début de l'exécution du programme.

constexpr int FACTOS[] {
   Facto<0>::value, Facto<1>::value, Facto<2>::value,
   Facto<3>::value, Facto<4>::value, Facto<5>::value,
   Facto<6>::value, Facto<7>::value, Facto<8>::value,
   Facto<9>::value, Facto<10>::value, Facto<11>::value,
   Facto<12>::value
};

La stratégie derrière la politique base_factorielle sera structurellement conforme à celle de base_reguliere mais son implémentation sera suffisamment distincte pour exiger un raffinement de la stratégie d'affichage.

À la construction, une politique base_factorielle évaluera le plus grand entier pour lequel une unité factorielle sera viable.

Par la suite, les méthodes prochaine(), courante() et est_signifiante() travailleront de concert pour identifier les unités à exprimer en base factorielle.

La stratégie globale présume par contre que les unités trouvées s'afficheront dans l'ordre, de la plus significative à la moins significative, ce qui constitue une modification à l'algorithme utilisé pour base_reguliere.

template <auto N>
   struct base_factorielle {
      using prod_categ_t = iteratif_tete;
   private:
      static_assert(N < std::size(FACTOS));
      int nbpos = 0;
   public:
      constexpr bool est_signifiante(int) {
         return --nbpos != 0;
      }
      constexpr int evaluer_factorielle(int n) const {
         return FACTOS[n];
      }
      constexpr int plus_grand_facto_viable(int n) const {
         int candidat = static_cast<int>(std::size(FACTOS));
         while(candidat && evaluer_factorielle(--candidat) > n)
            ;
         return candidat;
      }
      constexpr int prochaine(int n) const {
         return n < evaluer_factorielle(nbpos) ? n :
                n ? n % evaluer_factorielle(
                       plus_grand_facto_viable(n)
                    ) : 0;
      }
      constexpr int courante(int n) const {
         return n < evaluer_factorielle(nbpos)? 0 :
                n / evaluer_factorielle(
                   plus_grand_facto_viable(n)
                );
      }
      base_factorielle() = default;
      constexpr base_factorielle(int n)
         : nbpos{ plus_grand_facto_viable(n) } {
      }
   };
#endif

Ici, le type interne et public prod_categ_t d'une politique d'affichage selon une base de numération donnée sera un type vide et instanciable par défaut., et la seule particularité de chaque type du genre sera d'être vide et différent des autres. Chaque type représentera une stratégie algorithmique distincte.

Intégrer le tout

Reste à voir comment intégrer base factorielle, base régulière et stratégie d'affichage selon une base de numération donnée dans un tout cohérent, opérationnel et efficace.

#ifndef AS_BASE_H
#define AS_BASE_H

#include "base_reguliere.h"
#include "base_factorielle.h"
#include "alphabet_traits.h"
#include <iosfwd>
#include <string>
template <int N, class C = char, template <int> class OpBase = base_reguliere>
   class as_base_impl {
      using oper_t = OpBase<N>;
      using char_type = C;
      using str_type = std::basic_string<char_type>;
      using ostream_type = std::basic_ostream<
         char_type, std::char_traits<char_type>
      >;
      str_type prefixe;
      ostream_type &os;
      oper_t oper;
      int val;
      static constexpr auto TAILLE_ALPHABET =
         alphabet_traits<char_type>::TAILLE;
      static constexpr int NB_UNITES_CHIFFRES = '9' - '0' + 1;
      static_assert(
         strictement_positif_v<N> &&
         N < (NB_UNITES_CHIFFRES + TAILLE_ALPHABET),
        "base invalide"
      );
      static constexpr char_type symbole(int n) {
         static constexpr char_type BASE_CHIFFRES('0');
         static constexpr char_type BASE_LETTRES('a');
         return n < NB_UNITES_CHIFFRES?
            static_cast<char_type>(n + BASE_CHIFFRES) :
            static_cast<char_type>(BASE_LETTRES + n - NB_UNITES_CHIFFRES);
      }
      void affichage_brut(int n, recursif_queue prod) {
         if (oper.est_signifiante(n))
            affichage_brut(oper_.prochaine(n), prod);
         os << symbole(oper.courante(n));
      }
      void affichage_brut(int n, iteratif_tete) {
         os << symbole(oper_.courante(n));
         n = oper.prochaine(n);
         while (oper.est_signifiante(n)) {
            os << symbole(oper.courante(n));
            n = oper.prochaine(n);
         }
      }
   public:
      as_base_impl(int n, ostream_type &os, const str_type &prefixe = base_traits<N>::prefixe(), oper_t oper = {})
         : val{ n }, os{ os }, prefixe{ prefixe }, oper{ OpBase<N>(n) } {
      }
      void operator()() {
         if (val) {
            os << prefixe;
            affichage_brut(val, typename oper_t::prod_categ_t{});
         } else {
            os << 0;
         }
      }
   };
template <
   int N, template <int> class OpBase = base_reguliere,
   class C = char
>
   class as_base {
      int val;
      using char_type = C;
      using str_type = std::basic_string<char_type>;
      using ostream_type = std::basic_ostream<
         char_type, std::char_traits<char_type>
      >;
      str_type prefixe;
      OpBase<N> oper;
   public:
      as_base(int n, OpBase <N> oper = {}, const str_type &prefixe = base_traits<N, char_type>::prefixe())
         : val{ n }, oper{ oper }, prefixe{ prefixe } {
      }
      ostream_type& operator()(ostream_type &os) const {
         as_base_impl<
            N, char_type, OpBase
         > aff(val, os, prefixe, oper);
         aff();
         return os;
      }
   };
template <int N>
   as_base<N> afficher_base(int n) {
      return as_base<N>(n);
   }
template <int N, class C>
   as_base<N, base_reguliere, C>
      afficher_base(int n, const std::basic_string<C> &) {
      return { n };
   }
template <int N, class C>
   as_base<N, base_reguliere, C> afficher_base(int n, const C *prefixe) {
      return { n, base_reguliere<N>(), prefixe };
   }
template <int N, template <int> class OpBase, class C>
   auto& operator<<(std::basic_ostream<C, std::char_traits<C>> &os,
                    const as_base<N, OpBase, C> &aff) {
      return aff(os);
   }

#endif

À noter :

Pour le reste, tout demeure tel quel.


Valid XHTML 1.0 Transitional

CSS Valide !