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.
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. |
|
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.
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. |
|
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).
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. |
|
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. |
|
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. |
|
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é :
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. |
|
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()). |
|
Pour l'aiguillage statique des algorithmes, retenez simplement que nous expliquerons la manoeuvre en temps en lieu.
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 :
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. |
|
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). |
|
Le foncteur aura recours à quatre attributs d'instances :
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. |
|
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. |
|
Notre exigence quant aux valeurs acceptables des bases entières est définie par une assertion statique, comme il se doit. |
|
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. |
|
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. |
|
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). |
|
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. |
|
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. |
|
À 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. |
|
Une deuxième simplification serait de déduire le type de caractère à utiliser du type du préfixe suppléé par le code client. |
|
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>. |
|
Enfin, pour faciliter l'enchaînement des affichages sur un flux, un manipulateur de flux spécialisé pour nos objets peut être offert. |
|
Voilà. Nous avons un afficheur opérationnel en fonction de bases traditionelles. Attaquons maintenant le problème de la base x.
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...
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. |
|
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. |
|
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. |
|
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.
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.