Les littéraux utilisateur

Andrzej Krzemieński

Traducteur

Bousk

Trois articles au sujet d'une nouveauté du langage : la possibilité de définir des littéraux utilisateur.

Vous pouvez réagir au contenu de ce tutoriel sur le forum C++ : Commentez

- Titre : Les littéraux utilisateur

- Auteur : Andrzej Krzemieński

- Traducteur : Bousk

- Parution : 17 juillet 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Andrzej Krzemieński. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Métaprogrammation et métafonctions en C++11

Sommaire

- I. Les littéraux préparés

- I-A. Les littéraux prédéfinis

- I-B. Objectifs des littéraux utilisateur

- I-C. Les littéraux préparés (cooked literals)

- I-D. Quelle est l'utilité ?

- I-E. Remerciements

- II. Littéraux bruts

- II-A. Introduction

- II-B. les opérateurs littéraux bruts

- II-C. Templates d'opérateurs littéraux

- II-D. Remerciements

- III. Aller plus loin

- III-A. Introduction

- III-B. Un peu de métaprogrammation

- III-C. Le littéral binaire ultime

- III-D. Une note sur la performance

- III-E. Espaces de noms littéraux

- III-F. Les littéraux de chaîne de caractères définis par l'utilisateur

- III-G. Repousser les limites

- III-H. Remerciements

I - Les littéraux préparés

Cet article traite des littéraux en général, le but et l'utilité des littéraux utilisateur, leurs limites et les autres possibilités offertes en C++ pour un objectif similaire.

I-A - Les littéraux prédéfinis

Les littéraux permettent de positionner des objets appartenant aux types prédéfinis, avec des valeurs connues à la compilation.

int i = 0;
i = 17;

D'autres manières pour définir une valeur existent :

int i{}; // zéro-initialisation
i = {}; // réinitialise à l'état zéro-initialisé

Dans leur forme la plus simple (sans aucun préfixe ni suffixe), les littéraux définissent la valeur et le type de base :

// code C++ non valide, mais de sens évident
decltype(11) == int;
decltype('y') == char;
decltype("dog") == const char[3+1];
decltype(true) == bool;
decltype(nullptr) == nullptr_t;

Il existe des mots-clés pour certains littéraux (true, false, nullptr), puisque les valeurs possibles sont peu nombreuses. C'est également le cas pour les enum: généralement le nombre de valeurs d'un enum est assez faible pour que le compilateur puisse stocker la valeur associée à chaque littéral. Ces littéraux présentent peu d'intérêt pour ce billet. Pour les autres types de littéraux (entier, flottant, chaîne de caractères, caractère), les valeurs possibles sont bien trop nombreuses pour que le compilateur puisse les stocker dans un tableau associatif. Il va donc les reconnaître d'après un modèle de correspondance (une suite de chiffres pour un entier, une suite de caractères entre guillemets pour une chaîne de caractères, etc.).

En préfixant ou suffixant un littéral, nous pouvons influer sur son type :

// code C++ non valide, mais de sens évident
decltype(11) == int;
decltype(11UL) == unsigned long;
decltype(11LL) == signed long long;
decltype( 'y') == char;
decltype(u'y') == char16_t;
decltype(U'y') == char32_t;
decltype(L'y') == wchar_t;
decltype( "dog") == const char[3 + 1];
decltype(u"dog") == const char16_t[3 + 1];
decltype(U"dog") == const char32_t[3 + 1];
decltype(L"dog") == const wchar_t[3 + 1];

Ceci est très utile pour utiliser la bonne surcharge de fonction ou correctement déduire le type d'une variable :

void fun(int i);
void fun(unsigned i);
fun(12);  // utilise la première surcharge
fun(12U); // utilise la seconde surcharge
auto c1 =  'c'; // char
auto c2 = u'c'; // char16_t

Les préfixes ou suffixes peuvent également influer sur l'interprétation du littéral :

auto i1 = 80;    // entier 80
auto i2 = 0x80;  // entier 128
decltype(i1) == decltype(i2);
assert (i1 != i2);
auto s1 = "(\\\\)";  // affiche (\\)
auto s2 = R"(\\\\)"; // affiche \\\\
decltype(s1) == decltype(s2);
assert (s1 != s2);

Quand nous définissons à la compilation des valeurs pour les types définis par l'utilisateur, nous nous servons du fait que ces objets sont composés de types prédéfinis du langage, et nous combinons simplement des littéraux :

std::complex<double> j{0.0, 1.0}; // deux littéraux utilisés
BigInt I{1};                      // littéral entier converti en BigInt
boost::tribool b{true};           // littéral booléen converti en tribool
std::string s{"dog"};             // const char[4] converti en std::string

Ceci fonctionne pour initialiser des objets, mais nous ne pouvons choisir la surcharge correcte sans créer un objet temporaire ou réaliser une conversion explicite :

void fun(int i);
void fun(BigInt i);
fun(1);          // utilise la première surcharge
fun(BigInt{1});  // utilise la seconde surcharge

I-B - Objectifs des littéraux utilisateur

Les littéraux utilisateur peuvent être définis uniquement pour les entiers, flottants, caractères et chaînes de caractères. De nouveaux types sont définis en spécifiant de nouveaux suffixes (mais il n'est pas possible de créer de préfixe). Le but initial était, comme écrit dans les propositions, de fournir au comité de standardisation un outil pour créer de nouveaux types de littéraux via la bibliothèque standard, plutôt qu'en étendant le langage. Modifier la bibliothèque standard est perçu comme plus aisé. Le comité veut utiliser cet outil pour ajouter tous les nouveaux littéraux ajoutés, ou en passe de l'être, au C ou à des extensions standards du C. Par exemple les littéraux décimaux, comme 10.2df. Cet objectif a été seulement partiellement atteint, parce que de nombreux nouveaux littéraux nécessitent de nouveaux préfixes, ou une autre syntaxe qu'un préfixe ou suffixe :

- entiers binaires : 0b11011

- flottants hexadécimaux : 0x102Ap12

- Nouveaux caractères : u'A'

L'autre objectif des littéraux était de préparer l'arrivée de nouveautés au standard C++ : entiers de précision arbitraire, nombres décimaux flottants, décimaux à nombres significatifs fixés, nouveaux types de chaîne de caractères, unités du système international.

Alors que les littéraux utilisateur sont initialement le jouet du comité, tout programmeur a aussi un accès limité à cet outil.

I-C - Les littéraux préparés (cooked literals)

Il existe plusieurs manières de définir un nouveau suffixe de littéral. Sur cette partie, nous allons nous concentrer sur celle nommée les littéraux préparés (cooked literals). Supposons que notre programme traite des poids (l'unité physique)(1). Nous ne voulons pas utiliser de double parce que nous travaillons dans un environnement international et pour certains développeurs « poids » signifie kilogrammes, tandis que pour d'autres, il s'agit de livres. Nous introduisons donc un nouveau type Kilograms. Le nom du type indique clairement de quelle unité il s'agit. Nous empêchons la conversion depuis un double afin d'éviter toute confusion malheureuse d'unité de poids :

class Kilograms
{
double rawWeight;
public:
class DoubleIsKilos{}; // un tag
explicit constexpr Kilograms(DoubleIsKilos, double wgt) : rawWeight{wgt} {}
};
Kilograms wgt = 100.2; // erreur de compilation

Ce constructeur très moche évite l'emploi d'un poids à l'état brut en prévenant les conversions implicites et l'utilisation de double pour le poids. Maintenant le but est d'avoir cette syntaxe :

Kilograms wgt = 100.2_kg;

Il est impossible de définir un littéral sous la forme 100.2kg à cause d'une limitation du C++ : il est seulement possible de définir des suffixes qui commencent par un underscore « _ ». Ceci afin d'éviter la collision avec d'éventuels prochains suffixes ajoutés au standard. D'abord, regardons la syntaxe pour leur mise en place. Les explications suivront.

constexpr Kilograms operator "" _kg( long double wgt )
{
return Kilograms{Kilograms::DoubleIsKilos{}, static_cast<double>(wgt)};
}

Nous définissons une fonction particulière, tout comme nous définissons operator+=. Elle prend un long double en argument et retourne un Kilograms. La fonction est déclarée constexpr. Ce n'est pas nécessaire de faire fonctionner ce simple exemple, mais il s'agit généralement d'une bonne idée, parce que les littéraux sont souvent utilisés pour initialiser des constantes à la compilation. La partie operator "" _kg est une convention ; elle signifie « déclare le suffixe _kg ». Notez cependant que l'espace entre "" et _kg est essentiel : sinon ""_kg est interprété comme une chaîne vide avec un suffixe _kg lors des premières étapes d'analyse du code source.

Autre chose à noter : même si nous stockons des double, nous prenons des long double en argument et réalisons la conversion. Ceci est une particularité des littéraux préparés : vous êtes obligés de considérer le type le plus large possible dans sa « catégorie » : pour un nombre à virgule, il s'agit de long double ; pour un entier ce sera un unsigned long long. Oui, unsigned, parce que le signe ne fait pas partie d'un littéral: il s'agit d'un opérateur unaire appliqué à la variable temporairement initialisée à partir de notre littéral.

Nous pouvons très bien déclarer plusieurs suffixes retournant le même type. Par exemple, en plus des Kilograms, nous pouvons fournir un suffixe pour permettre aux programmeurs qui pensent en livres à utiliser un suffixe livre :

constexpr Kilograms operator "" _lb( long double wgt )
{
return Kilograms{Kilograms::DoubleIsKilos{}, static_cast<double>(wgt * 0.45359237)};
}
Kilograms wgt = 200.5_lb;

Et vous pouvez combiner les deux sans soucis :

Kilograms w = 200.5_lb + 100.1_kg; // ok (si l'on définit operator+)
Kilograms v = 21000.33 + 100.1_kg; // erreur: addition de Kilograms et double impossible

Comment fonctionne un littéral préparé ? Quand le compilateur rencontre le code 100.1_kg il reconnaît un flottant avec un suffixe inconnu. À ce niveau, en C++03 le compilateur s'arrêterait et remonterait une erreur ; un compilateur C++11 va désormais chercher parmi tous les suffixes utilisateur connus, si le suffixe existe. S'il trouve le suffixe correspondant et remarque que vous utilisez un littéral préparé, il interprétera 100.1 comme un flottant du plus large type possible (long double) et le passe en argument à votre fonction. C'est pour cela qu'on nomme ces littéraux préparés : vous n'avez pas de vous soucier d'analyser un par un les chiffres qui précèdent votre suffixe, le compilateur le fait pour vous. Maintenant, ce que vous pouvez faire, c'est changer le type de paramètre ou transformer sa valeur.

Voyons un autre exemple qui démontrera une caractéristique, ou une limitation, des littéraux préparés. Nous définissons un type qui représente une probabilité : son type de base sera long double, mais seules les valeurs [0,1] seront valides :

class Probability
{
long double value;
// invariant:  0.0 <= value && value <= 1.0
public:
explicit constexpr Probability(long double v);
// ...
};

Dans la définition de notre fonction constexpr, nous utilisons à la fois des vérifications lors de la compilation et à l'exécution, comme décrit dans Compile-time computations :

constexpr Probability operator"" _prob( long double v )
{
return v > 1.0 ? throw BadProbability{v} : Probability{v};
}

Remarquez que nous ne vérifions pas l'inégalité v < 0 puisqu'un littéral n'est jamais négatif. Maintenant, nous nous attendons à ce que le code suivant génère une erreur à la compilation :

Probability p = 1.2_prob;

Cette attente est sensée : la valeur de la constante est toujours connue à la compilation ; il devrait donc être facile de modifier sa valeur à la compilation. Cependant, comme c'est le cas pour operator+=, un opérateur (y compris un opérateur de littéral) est juste un appel de fonction, et notre ligne équivaut à :

Probability p = operator"" _prob(1.2);

Ainsi, comme tout autre opérateur constexpr, il n'est pas évalué à la compilation (même s'il est appelé avec des constantes connues à la compilation) à moins que vous initialisiez un autre littéral lors de la compilation :

constexpr Probability p = 1.2_prob; // erreur à la compilation

Un autre exemple, considérez un suffixe de chaîne de caractères qui transforme un const char* en std ::string :

std::string operator"" _s(const char * str, size_t len)
{
return std::string{str, len};
}
void fun(const char*);
void fun(std::string);
fun("dog"_s); // picks second overload

Ceci est un autre exemple des littéraux préparés. Le second paramètre (len) est requis par la logique des opérateurs de littéraux en C++, mais nous n'avons pas besoin de l'utiliser. Être préparé dans ce cas signifie que tous les caractères d'échappement ou séquences particulières dans une chaîne brute sont traités avant que notre opérateur soit appelé :

auto s1 = "\\dog\\"_s;  // renders: \dog\
auto s2 = R"(\dog\)"_s; // renders: \dog\
assert (s1 == s2);

I-D - Quelle est l'utilité ?

Jusque-là nous n'avons parlé que de la forme des littéraux préparés, et laissé les autres formes (les littéraux bruts) pour le post suivant. La question est désormais leur utilité et la justification de leur ajout au langage.

Considérez l'exemple suivant utilisant des chaînes de caractères. Nous pouvons choisir la bonne surcharge à appeler en créant une temporaire explicitement (plutôt que dans l'opérateur de littéral) :

void fun(const char*);
void fun(std::string);
fun(std::string{"dog"}); // appel la seconde surcharge

C'est un peu plus long, mais suffisant et de nombreuses personnes pourraient trouver cette écriture plus claire et moins offusquée : tout le monde sait comment et pourquoi vous créez une variable temporaire.

Pour revenir à notre exemple, je vois au moins deux substituts aux littéraux. Première option, nous pouvons utiliser des fonctions plutôt que l'opérateur de littéral :

Kilograms w = pounds(200.5) + kilograms(100.1);

Cette approche a été adoptée pour l'en-tête actuel de <chrono> (vous pouvez avoir une courte introduction et pour une proposition originelle). Avec <chrono> vous pouvez spécifier chaque durée de cette manière :

#include <chrono>
using namespace std::chrono;
auto duration1 = hours(8) + minutes(30) + seconds(5) + milliseconds(120);

C'est un peu plus verbeux que l'utilisation de suffixes de littéraux, mais est très facilement lisible : vous ne pouvez pas mal interpréter cette déclaration.

Avec le C++14, les littéraux utilisateur ont été ajoutés sur chrono, complex, et string et sont définis dans des namespace de type inline.

Deuxième option en déclarant des constantes qui représentent les « unités », et utiliser ces constantes dans des multiplications avec des valeurs nominales :

constexpr Kilograms kg{Kilograms::DoubleIsKilos{}, 1.0}
constexpr Kilograms lb{Kilograms::DoubleIsKilos{}, 0.45359237};
Kilograms w = 200.5 * lb + 100.1 * kg;

Cette approche a été adoptée par la bibliothèque Boost.Units, dans laquelle les unités physiques sont représentées par des constantes :

using namespace boost::units;
using namespace boost::units::si;
quantity<force>  F  = 2.0 * newton;
quantity<length> dx = 2.0 * meter;
quantity<energy> E1 = F * dx;
quantity<energy> E2 = 2.0 * newton  *  2.0 * meter;

L'opérateur multiplication est plutôt intuitif à comprendre ici, parce qu'en physique deux symboles collés sont déjà interprétés comme une multiplication, même si nous multiplions une unité par un scalaire : 3m signifie « l'unité du mètre multipliée par un facteur de 3 », ou « 3 fois m ».

Les littéraux nous rapprochent de la notation physique : E = 2,0N . 2,0m. Mais est-ce que cet objectif vaut une complication du langage ? Nous pouvons aussi signaler que les opérateurs +, *, ne sont pas non plus nécessaires, et nous pourrions nous en passer en utilisant des appels de fonctions :

Kilograms v = add(mul(a, b), c);

Et c'est ce que nous serions obligés de faire si le langage ne proposait pas la surcharge d'opérateur pour manipuler un type défini par l'utilisateur. Mais vous pouvez voir que le manque d'opérateurs arithmétiques perturbe. Avec les unités du système international (SI) la situation est différente. D'abord, moins de programmeurs en ont besoin ; ensuite, il y a une limite sur la forme dans laquelle nous pouvons les représenter, parce que certaines nécessitent un symbole qui sort de la table de caractères classique d'un code source, par exemple µs.

Il y a cependant une utilité pratique à l'utilisation de littéraux préparés. Comme les littéraux numériques ne représentent jamais un nombre négatif, vous pouvez facilement éliminer toutes les valeurs négatives à la compilation d'un type que vous déclarez et n'ayant pas de représentation négative. C'est ce que nous avons fait pour notre type Probability plus haut :

double d = -0.5;          // ok: operator-(0.5);
Probability p = -0.5_prob; // erreur: operator- pas défini pour Probability

C'est tout pour le moment. La partie 2 traitera des littéraux bruts.

I-E - Remerciements

Toute l'équipe de Developpez.com remercie sincèrement Andrzej Krzemieński qui nous a aimablement permis de traduire et de publier son tutoriel sur notre site. Nous remercions aussi Bousk d'avoir fait la traduction, Joly Loïc et Luc Hermitte pour leurs retours techniques et Claude Leloup pour les corrections orthographiques.

II - Littéraux bruts

Cet article traite des littéraux en général, le but et l'utilité des littéraux utilisateur, leurs limites et les autres possibilités offertes en C++ pour un objectif similaire.

II-A - Introduction

Dans la partie précédente sur les littéraux définis par l'utilisateur, nous avons vu ce qu'ils sont et comment définir des opérateurs. À savoir, là où le compilateur voit le littéral 12_kg, il extrait la valeur 12 de type long double et appelle votre fonction operator""  _kg(12.L) pour transformer le résultat. Dans ce billet, nous allons explorer d'autres aspects des littéraux définis par l'utilisateur : les opérateurs littéraux bruts, lesquels permettent d'inspecter chaque caractère du littéral.

II-B - les opérateurs littéraux bruts

Quelquefois, l'opérateur littéral préparé ne conviendra pas, et nous aurons à analyser les littéraux caractère par caractère. Nous avons besoin de cela quand le suffixe change l'interprétation des chiffres du littéral. Par exemple, le nombre 11 signifie quelque chose quand il est interprété comme un nombre en base 10 et autre chose quand il est interprété en base 2. Si on utilisait un opérateur littéral préparé, le compilateur l'interpréterait comme onze et on ne saurait jamais s'il s'agit du résultat d'analyse lexicale de 11, 0xB, ou la valeur 0 (13).

Similairement, considérons un type défini par l'utilisateur pour stocker des nombres décimaux. Votre type est capable de stocker le nombre réel 10.2 sans aucune perte de précision. Cependant, si vous utilisez un l'opérateur littéral préparé, le compilateur devra convertir votre type en long double, mais ce type n'est pas capable de stocker la valeur 10.2 exactement, et devra utiliser quelque chose comme 10.19999999… à la place. Cette valeur à son tour ne peut être convertie en nombre décimal sans perte de précision. Dans ces cas, il est souhaitable de convertir la représentation chaîne du littéral directement dans le type et la valeur de destination sans étape intermédiaire.

Voyons comment nous pouvons parser un littéral caractère par caractère. Disons que nous voulons être capables d'utiliser des littéraux binaires, similaires à 0b11010110. Bien sûr, nous savons déjà que nous sommes limités : nous pouvons seulement définir un suffixe de littéral et un suffixe non standard doit commencer par un underscore. Nous allons donc vraiment parser des littéraux comme 11010110_b. Tout d'abord, nous prenons le plus simple, et la manière la moins attrayante. Nous allons définir un opérateur littéral brut avec la signature suivante :

unsigned operator"" _b(const char* str);

Le compilateur lit :

À chaque fois que tu trouves un nombre entier ou à virgule flottante avec le suffixe _b , tu le passes en paramètre comme une chaîne de style C à notre fonction et nous retournerons une valeur correspondante de type unsigned .

Notez la magie non disponible pour les opérateurs littéraux préparés. L'usage d'opérateur brut ne peut être remplacé par un appel de fonction normal. Ce qui est fait, c'est que quand le compilateur trouve un code comme :

auto i = 101_b;

Il le traite comme :

auto i = operator"" _b("101");

C'est quelque peu similaire à la solution suivante utilisant sur une macro :

unsigned str2int(const char* str);
# define BINARY(literal) str2int(#literal)
auto i = BINARY(101);

Notez également que cette forme de déclaration d'opérateur de littéral implique que cela fonctionne uniquement pour les nombres entiers et à virgule flottante et ne fonctionnera jamais avec les littéraux chaîne. Mais n'avons-nous pas utilisé une déclaration d'opérateur littéral dans le précédent billet ? Il était similaire, mais pas identique. De manière à définir un opérateur littéral chaîne préparé, nous devons écrire :

unsigned operator"" _s(const char * str, size_t len);

Notez le second argument len. Il nécessite d'être de type entier (pas nécessairement size_t). Un des premiers usages (bien que pas le seul) pour ce second argument est de différencier un opérateur littéral chaîne préparé d'un opérateur littéral brut numérique (entier ou virgule flottante). Cette « astuce » est similaire à la déclaration posfix operator++ ;

Integral operator++(Integral& i, int);

Là, le second argument est seulement requis pour homonymie syntaxique. Mais revenons à notre première tentative d'opérateur entier binaire littéral. Nous pouvons l'implémenter comme ceci :

unsigned operator"" _b(const char* str)
{
unsigned ans = 0;

for (size_t i = 0;  str[i] != '\0';  ++i) {
// Variant de boucle: strlen(str + i);
char digit = str[i];
if (digit != '1' && digit != '0') {     // (1)
throw std::runtime_error("on autorise seulement des 0 et des 1");
}
ans = ans * 2 + (digit - '0');          // (2)
}
return ans;
}

Les points clés à observer :

1. Nous autorisons seulement des 0 et des 1 dans chaque caractère du littéral ;

2. À chaque itération, nous « faisons croître » la valeur à retourner (ans) par le nouveau chiffre lu.

Voici comment nous pouvons tester notre nouvel opérateur littéral :

int main() try
{
unsigned i = 101_b;
assert (i == 5);
unsigned j = 123_b;
assert (false); // on ne devrait jamais arriver ici
}
catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}

Notre opérateur nécessite un test en plus : le littéral binaire est-il trop long ? Si nous supposons que le type unsigned a 32 bits, nous ne devrions pas autoriser de littéraux binaires plus longs que ce nombre. Nous l'avons omis pour le moment, mais allons l'ajouter dans nos prochaines tentatives pour améliorer notre opérateur littéral. Pourquoi a-t-il besoin d'amélioration ? Tout d'abord, car ce n'est pas une fonction constexpr, nous ne pouvons pas l'utiliser comme constante à la compilation.

constexpr unsigned i = 11011_b; // ERREUR
static_assert(101_b == 5, "!"); // ERREUR
int array_of_ints[ 11011_b ];   // ERREUR

Afin de faire de notre opérateur une fonction constexpr, nous devons abandonner les itérations en boucle(2) en faveur de la récursion, et recourir à une façon un peu plus évoluée pour signaliser une entrée invalide, comme décrit ci-dessous :

template <typename T>
constexpr size_t NumberOfBits()
{
static_assert(std::numeric_limits<T>::is_integer, "Seuls les entiers sont autorisés");
return std::numeric_limits<T>::digits;
}

constexpr size_t length( const char * str, size_t current_len = 0 )
{
return *str == '\0' ? current_len           // fin de récursion
: length(str + 1, current_len + 1);    // calcule récursivement
}

constexpr bool is_binary( char c )
{
return c == '0' || c == '1';
}

size_t TOO_LONG_BINARY_LITERAL()
{
throw std::runtime_error("Littéral binaire trop long");
}

size_t ONLY_0_AND_1_IN_BINARY_LITERAL()
{
throw std::runtime_error("Seuls les 0 et les 1 sont autorisés dans un littéral binaire");
}
constexpr unsigned build_binary_literal( const char * str, size_t val = 0 )
{
return length(str) == 0 ? val                                // fin de récursion
: !is_binary(*str) ? ONLY_0_AND_1_IN_BINARY_LITERAL()   // test de chiffre non binaire
: build_binary_literal(str + 1, 2 * val + *str - '0');  // inspecte récursivement
}
constexpr unsigned operator"" _b( const char * str )
{
return length(str) > NumberOfBits<unsigned>()                // Littéral trop long ?
? TOO_LONG_BINARY_LITERAL()                             // rapport d'erreur
: build_binary_literal(str);                            // construit une valeur numérique
}
static_assert(10001000100010001000100010001000_b == 0x88888888, "!!");

La plupart des astuces utilisées ici sont décrites dans mon autre post : “Compile-time computations.”

Maintenant, notre littéral binaire peut être utilisé pour créer des constantes compile-time; cependant, cela ne garantit pas que le littéral sera toujours évalué en compile-time :

int main()
{
unsigned i = 1102_b; // compile, mais lance une exception à l'exécution
}

II-C - Templates d'opérateurs littéraux

De façon à s'assurer que notre littéral soit toujours évalué au moment de la compilation, nous allons utiliser une autre forme d'opérateur littéral brut :

template <char... STR> constexpr unsigned operator"" _b();

Les templates à nombre d'arguments variable vous sont-ils familiers ? La notation char … indique que ce template peut être instancié avec 0, 1, 2, ou plus paramètres de type char. La déclaration ci-dessus signifie que chaque fois que le compilateur rencontre un littéral tel que 11011_b, il devrait le traiter comme l'appel de fonction suivant :

operator"" _b<'1', '1', '0', '1', '1'>();

Notez qu'il n'y a pas de terminaison '\0'. Maintenant, la chaîne entière représentant le littéral est passée (hachée) comme argument de template. Ceci offre la possibilité d'inspecter tous les caractères au moment de la compilation, quel que soit le contexte dans lequel le littéral est utilisé. Comment l'opérateur peut-il être implémenté ? Un certain nombre de personnes ont déjà décrit comment un littéral binaire peut être implémenté en C++11. Par exemple, Daniel Krügler (voir ici) et Johannes Schaub (voir ici). Ci-dessous, nous allons analyser une implémentation simple, pas à pas.

L'évaluation à la compilation nécessite que nous utilisions encore la récursivité. La récursion est généralement requise lorsqu'on traite un nombre variable de templates. La récursion en métaprogrammation template (que nous sommes sur le point d'utiliser) nécessite que les cas de terminaison de récursivité soient implémentés via une spécialisation par template.

template <unsigned VAL>                             // (D) termine la récursion
constexpr unsigned build_binary_literal()
{
return VAL;
}
template <unsigned VAL, char DIGIT, char... REST>   // (B) génération récursive de valeur
constexpr unsigned build_binary_literal()
{
static_assert(is_binary(DIGIT), "Seuls les 0 et les 1 sont autorisés");
return build_binary_literal<(2 * VAL + DIGIT - '0'), REST...>(); // (C)
}
template <char... STR>
constexpr unsigned operator"" _b()
{
static_assert(sizeof...(STR) <= NumberOfBits<unsigned>(), "littéral binaire trop long");
return build_binary_literal<0, STR...>();        // (A)
}

Ce court exemple de code mérite une longue explication. Tout d'abord, notez qu'avec la version utilisant la métaprogrammation de template, le rapport d'erreur est plus propre, puisque nous pouvons utiliser static_assert à la place de certaines astuces utilisant des sous-expressions non constantes dans des expressions constantes . Notez l'entité STR dans la définition de l'opérateur littéral template. Ce n'est pas une valeur unique. C'est un regroupement de paramètres template . Il représente tous les caractères (char) (0, 1, 2, ou plus) avec lesquels notre template peut être instancié. La notation STR… au point (A) est un bloc d'extension : il dit que le compilateur devrait reconstruire la même liste d'arguments que celle avec laquelle notre template a été instancié. Par exemple, si nous parsons le littéral 11011_b et que notre littéral template a été instancié comme ceci :

operator"" _b<'1', '1', '0', '1', '1'>();

L'expansion du paquet au point (A) devrait se traduire par l'instanciation du template build_binary_literal :

build_binary_literal<0, '1', '1', '0', '1', '1'>();

Notez aussi l'expression sizeof…(STR). Elle nous dit combien de chars sont vraiment dans le bloc de paramètres STR. Andrei Alexandrescu soutient dans cette conférence que l'opérateur sizeof... est une caractéristique inutile du langage, car on peut l'implémenter sous forme de bibliothèque en utilisant un template récursif (similaire au nôtre ci-dessus). Je crois qu'avec le support natif en tant que fonctionnalité du langage sizeof… peut être plus rapide et ne pas nécessiter nombre d'instanciations de template pas si petit que ça. Le C++ compile déjà lentement.

Notez le deuxième et le troisième argument template au point (B) : char DIGIT, char … REST. C'est comme cela qu'est implémentée la décomposition du paquet de paramètres. Nous disons que nous allons inspecter le premier caractère du paquet dans cette itération (DIGIT) et inspecter le reste (REST) dans la suite des appels récursifs. Cet appel récursif est effectué au point (C). Notez que le paquet est plus court d'un caractère par itération. Notez aussi que quand nous inspectons le dernier caractère du littéral (dans le template primaire), DIGIT contient sa valeur et REST est vide. Quand nous instancions le template dans cette itération finale, c'est équivalent à :

build_binary_literal<(2 * VAL + DIGIT - '0')>();

Notez la disparition magique de la virgule entre (2 * VAL + DIGIT - '0') et le paquet de paramètres désormais vide REST. Ceci appelle la spécialisation template au point (D), laquelle arrête la récursion.

Avec l'opérateur littéral défini sous forme de template, nous sommes sûrs que nos littéraux binaires seront toujours testés au moment de la compilation :

int main()
{
unsigned i = 102_b; // compile-time error
}

Ce template d'opérateur littéral ainsi défini a toujours une limitation. Dans le cas où le littéral nécessite plus que 32 bits (je suppose sizeof(unsigned) == 4 et un char de 8 bits) nous obtenons une erreur lors de la compilation. Dans le cas d'une longueur de 40 bits, il serait mieux que le littéral binaire retourné soit d'un type unsigned. C'est un peu similaire à la manière dont les littéraux de base fonctionnent en C++ (jusqu'à C++03) :

auto i = 0x12345678;   // decltype(i) == int  (sur ma plate-forme)
auto j = 0x1234567890; // decltype(j) == long long int

Ceci est faisable en C++11, mais nous laissons cela pour le prochain billet.

II-D - Remerciements

Toute l'équipe de Developpez.com remercie sincèrement Andrzej Krzemieński qui nous a aimablement permis de traduire et de publier son tutoriel sur notre site. Nous remercions aussi Bousk d'avoir fait la traduction, Joly Loïc et Luc Hermitte pour leurs retours techniques et Claude Leloup pour les corrections orthographiques.

III - Aller plus loin

Cet article traite des littéraux en général, le but et l'utilité des littéraux utilisateur, leurs limites et les autres possibilités offertes en C++ pour un objectif similaire.

III-A - Introduction

Dans la partie précédent, nous avons vu comment définir un template d'opérateur littéral brut qui nous permet de convertir lors de la compilation presque tout littéral binaire de format 11011_b en une valeur de type unsigned int et de pouvoir utiliser cette même valeur comme constante lors de la compilation. Toutefois, la longueur du littéral devrait être suffisamment petite pour rentrer dans la capacité du type unsigned int. Comme promis, dans cette partie nous essayerons de faire en sorte que notre litteral retourne des valeurs de différents types en fonction de la taille du littéral binaire, de sorte que 11011_b retourne une valeur de type unsigned int et 100010001000100010001000100010001000_b retourne une valeur de type long long unsigned int.

III-B - Un peu de métaprogrammation

Un opérateur littéral est une fonction (template). Est-il possible d'avoir des fonctions qui retournent différents types de valeurs en fonction des différents types d'entrées? Grâce aux templates de fonction, cela est possible. Voici un exemple assez court qui montre comment faire. D'abord, nous montrons comment choisir différents types, en fonction de la valeur lors de la compilation :

1. template <bool COND>

2. struct Bool2Type_

3. {

4. using type = int;                                 // (1)

5. };

6. template <>

7. struct Bool2Type_<true>

8. {

9. using type = std::string;

10. };

11. template <bool COND>

12. using Bool2Type = typename Bool2Type_<COND>::type;  // (2)

Ainsi, nous avons implémenté un template de métafonction, comme décrit ici. Si on lui passe la valeur false, elle retourne un type int ; si on lui passe true, elle retourne le type std::string :

1. // non autorisé en C++, mais vous comprenez ce que cela veut dire

2. Bool2Type<false> == int;

3. Bool2Type<true>  == std::string;

Comment est-ce que l'implémentation marche ? La première déclaration de template déclare un template primaire. La ligne au point (1) est une déclaration d'alias. C'est presque la même chose qu'une déclaration de typedef, sauf qu'on spécifie d'abord le nom du nouveau type. Ensuite, nous avons une spécialisation du template pour la valeur true, qui contiendra un alias imbriqué type, référençant std::string. Ces deux déclarations suffisent pour dire que nous avons défini un template de métafonction, mais pour empêcher les utilisateurs de saisir le verbeux typename Bool2Type_<COND>::type, nous introduisons un autre template d'alias. C'est en cela que les alias sont supérieurs aux typedef : vous ne pouvez pas créer des templates de typedef.

C'est de cette façon que nous pouvons choisir un type. Maintenant, le code qui suit montre comment nous pouvons choisir à la fois un type et une valeur en utilisant un template de fonction :

1. template <bool COND>

2. Bool2Type<COND> bool2val()

3. {

4. return 0;

5. }

6. template<>

7. Bool2Type<true> bool2val<true>()

8. {

9. return std::string{"one"};

10. }

11. int main()

12. {

13. assert (bool2val<false>() == 0);

14. assert (bool2val<true>() == "one");

Mais un bon langage de programmation est fourni avec une bibliothèque standard qui facilite certaines tâches courantes. En C++11, la métaprogrammation est considérée comme tâche courante (que cela nous plaise ou non). Nous disposons déjà d'une métafonction pour choisir entre deux types :

1. using T0 = typename std::conditional<false, std::string, int>::type;

2. using T1 = typename std::conditional<true, std::string, int>::type;

3. // non C++:

4. T0 == int;

5. T1 == std::string;

Vous pouvez remarquer que std::conditional ressemble à un if pour choisir un des deux types. Vous n'avez pas besoin d'utiliser la valeur littérale true pour sélectionner le type, vous pouvez utiliser toute expression convertible en bool lors de la compilation :

1. using TX = typename std::conditional<sizeof(short) == sizeof(int), short, int>::type;

Une fois de plus, pour éviter de saisir tout cela, introduisons un autre template d'alias afin de simplifier l'utilisation de conditional. Nous allons l'utiliser plus tard :

1. template <bool COND, typename T, typename F>

2. using IF = typename std::conditional<COND, T, F>::type;

3. // utilisation :

4. using TX2 = IF<sizeof(short) == sizeof(int), short, int>;

III-C - Le littéral binaire ultime

Tout d'abord, créons une métafonction qui choisira le type le plus approprié pour notre littéral binaire, en fonction de sa taille. Comme dans le billet précédent, nous supposerons que nous utilisons une plateforme sur laquelle le type char a une taille de 8 bits. Une option serait de choisir un des types standard suivants : uint8_t, uint16_t, uint32_t, uint64_t. Pourtant, cela n'est pas très pratique. Nous allons procéder autrement. Commençons par unsigned, avec la taille d'un mot sur la plateforme native ; si elle est trop petite, nous allons essayer long unsigned, et si c'est toujours trop petit, nous utiliserons long long unsigned. Si ce dernier ne correspond toujours pas, nous laissons tomber. Cela peut être résumé par le pseudo-code suivant :

1. if (SIZE > sizeof(long long unsigned) * 8) {

2. ERROR();

3. }

4. if (SIZE <= sizeof(unsigned) * 8) {

5. return <unsigned>;

6. }

7. else {

8. if (SIZE <= sizeof(long unsigned) * 8) {

9. return <long unsigned>;

10. }

11. else {

12. return <long long unsigned>;

13. }

14. }

Nous pouvons l'implémenter comme ceci :

1. template <size_t SIZE>

2. struct select_type

3. {

4. template <typename T>

5. constexpr size_t NumberOfBits()

6. {

7. return std::numeric_limits<T>::digits;

8. }

10. static_assert(SIZE <= NumberOfBits<long long unsigned>(), "littéral binaire trop long");

12. using type = IF<(SIZE <= NumberOfBits<unsigned>()),

13. unsigned,

14. IF<(SIZE <= NumberOfBits<long unsigned>()),

15. long unsigned,

16. long long unsigned

17. >

18. >;

19. };

20. template <size_t SIZE>

21. using SelectType = typename select_type<SIZE>::type;

Permettez-moi de faire une petite digression ici. Typiquement, c'est une bonne idée d'écrire pour chaque morceau de code des tests unitaires qui vérifient le comportement de base du composant. Pour les fonctions exécutées lors de la compilation, la situation est particulière. Nous pouvons tester certains aspects en utilisant des assertions statiques :

1. static_assert(std::is_same<SelectType<1>, unsigned>::value, "!");

Mais certaines sémantiques des métaprogrammes ne peuvent pas être testées par des tests unitaires. Une des caractéristiques de notre métafonction est qu'elle signale une erreur (via l'échec de la compilation) lorsque la taille du littéral est trop grande pour qu'il puisse être converti au type long long unsigned. Vous pouvez vérifier cela manuellement, mais comment écrire un test unitaire pour cela, sans sortir du C++ ?

De retour au sujet, avec la fonction de sélection de type ci-dessus, notre mise en œuvre finale du template d'opérateur littéral est quelque peu similaire à celle décrite dans le billet précédent. La seule différence est que certains de nos templates nécessitent un type supplémentaire, qui précise le type unsigned que notre littéral obtiendra :

1. constexpr bool is_binary( char c )

2. {

3. return c == '0' || c == '1';

4. }

5. template <typename UINT, UINT VAL>

6. constexpr UINT build_binary_literal()

7. {

8. static_assert(std::is_unsigned<UINT>::value, "exige un type unsigned");

9. return VAL;

10. }

11. template <typename UINT, UINT VAL, char DIGIT, char... REST>

12. constexpr UINT build_binary_literal()

13. {

14. static_assert(is_binary(DIGIT), "seuls les 0 et les 1 sont permis");

15. static_assert(std::is_unsigned<UINT>::value, "exige un type unsigned");

16. return build_binary_literal<UINT, 2 * VAL + DIGIT - '0', REST...>();

17. }

18. template <char... STR>

19. constexpr SelectType<sizeof...(STR)> operator"" _b()

20. {

21. return build_binary_literal<SelectType<sizeof...(STR)>, 0, STR...>();

22. }

Maintenant, nous pouvons tester notre littéral :

1. static_assert(0_b == 0, "!!");

2. static_assert(1_b == 1, "!!");

3. static_assert(10_b == 2, "!!");

4. static_assert(1000100010001000100010001000100010001000_b == 0x8888888888, "!!");

5. int main()

6. {

7. auto i = 10001000100010001000100010001000_b;

8. auto j = 1000100010001000100010001000100010_b;

9. static_assert( std::is_same<decltype(i), unsigned int>::value, "!unsigned" );

10. static_assert( std::is_same<decltype(j), unsigned long long>::value, "!ull" );

11. }

III-D - Une note sur la performance

Notre littéral binaire a l'air cool, mais son utilisation a un prix. La valeur finale est disponible au moment de la compilation, donc cela ne demande pas de temps d'exécution supplémentaire ; cependant, vous pouvez trouver le temps supplémentaire de compilation étonnamment élevé. Même l'utilisation d'un seul littéral nécessite un certain nombre d'instanciations récursives de template. Je ne donne pas ici un point de référence, mais si vous essayez d'utiliser 32 littéraux, disons de taille 46, vous pouvez observer le ralentissement par rapport aux littéraux hexadécimaux intégrés. Si vous avez déjà utilisé des métaprogrammes lourds, vous savez combien ils peuvent ralentir la compilation. S'en tenir aux littéraux hexadécimaux peut rester la solution la plus attrayante, même s'il est possible de définir un littéral binaire.(3)

III-E - Espaces de noms littéraux

Les types de base peuvent être considérés comme définis dans l'espace de noms global tout comme les littéraux qui les représentent : vous n'allez jamais penser à les préfixer par un opérateur de résolution d'espace de noms. Cela ne fonctionnerait même pas. De même, l'opérateur de résolution d'espace de noms ne fonctionnera pas pour les littéraux définis par l'utilisateur, donc ils doivent être accessibles sans aucun qualificateur d'espace de noms. Il n'est pas possible d'utiliser de la recherche par dépendance aux arguments (argument-dependant lookup) parce que les opérateurs littéraux ne prennent que des arguments de types de base.

Vous pourriez envisager de définir vos opérateurs littéraux dans l'espace de noms global, mais cela engendre quelques problèmes. Premièrement, la définition de quelque chose dans l'espace de noms global est déjà risquée en temps normal. Les implémentations utilisent l'espace de noms global pour définir, en privé, certains composants d'infrastructure de bibliothèque . Ceux-ci commencent généralement par un underscore, et dans le cas des littéraux définis par l'utilisateur, nous sommes obligés de définir des suffixes commençant par un underscore. Deuxièmement, les suffixes littéraux ont tendance à être courts ce qui pourrait engendrer des conflits entre les littéraux des différentes bibliothèques. Par exemple, une bibliothèque peut utiliser le suffixe _lb pour un littéral binaire long, tandis qu'une autre peut utiliser _lb pour designer la masse en livres.

Alors, quelles sont les options dont dispose un auteur de bibliothèques ? Définir l'opérateur littéral directement dans l'espace de noms de la bibliothèque et importer les littéraux dans l'espace de noms global en utilisant la déclaration using :

1. // units_library.hpp:

2. namespace Units

3. {

4. class Mass;

5. Mass operator"" _lb(long double);

6. Mass operator"" _kg(long double);

7. }

8. using Units::operator"" _lb;

9. using Units::operator"" _kg;

Mais cela peut causer un conflit entre les littéraux si une autre bibliothèque déclare ses littéraux dans l'espace de noms global avec plus de négligence :

1. // utilities.hpp:

2. long operator"" _b(const char *);           // littéral binaire

3. unsigned long operator"" _lb(const char *); // littéral binaire long

1. // main.cpp:

2. # include "units_library.hpp"

3. # include "utilities.hpp"

4. // ERREUR: operator"" _lb est déjà déclaré dans l'espace de noms global

Remarquez que nous n'avons même pas essayé d'utiliser le littéral _lb. Une façon d'atténuer le problème c'est d'utiliser la directive using, plutôt que la déclaration using. Ainsi nous reportons l'erreur de compilation jusqu'à ce que quelqu'un essaie vraiment d'utiliser le littéral. Cependant, puisque la directive using « importe » chaque nom dans l'espace de noms, vous devriez séparer les définitions des opérateurs littéraux en les mettant dans un espace de noms supplémentaire :

1. // units_library2.hpp:

2. namespace Units

3. {

4. class Mass;

5. namespace operators

6. {

7. Mass operator"" _lb(long double);

8. Mass operator"" _kg(long double);

9. }

10. }

11. using namespace Units::operators;

C'est (un peu) mieux. Maintenant, l'erreur d'ambiguïté se produit uniquement lorsque nous essayons d'utiliser le littéral ambigu :

1. // main.cpp:

2. # include "units_library2.hpp"

3. # include "utilities.hpp"

4. // OK jusqu'ici

5. Units::Mass m1 = 200_kg; // OK

6. Units::Mass m2 = 400_lb; // ERREUR: ambiguïté

Cette technique est proposée dans N2750 (voir la section 3.5). Cependant, elle ne nous sauve toujours pas de l'ambiguïté dans le cas où deux bibliothèques définissent le même suffixe littéral. Une approche différente serait de toujours définir un espace de noms séparé (imbriqué) pour les littéraux de votre bibliothèque. N'importez jamais la bibliothèque dans l'espace de noms global. Demandez à l'utilisateur final de faire l'importation s'il veut utiliser des littéraux :

1. // units_library3.hpp:

2. namespace Units

3. {

4. class Mass;

5. namespace operators

6. {

7. Mass operator"" _lb(long double);

8. Mass operator"" _kg(long double);

9. }

10. }

1. // utilities_b.hpp:

2. namespace Utilities

3. {

4. namespace operators

5. {

6. long operator"" _b(const char *);           // littéral binaire

7. unsigned long operator"" _lb(const char *); // littéral binaire long

8. }

9. }

1. // main.cpp:

3. # include "units_library3.hpp"

4. # include "utilities_b.hpp"

5. void fun1()

6. {

7. using namespace Units::operators;

8. Units::Mass m1 = 2100_kg; // OK

9. Units::Mass m2 = 1100_lb; // OK

10. }

11. void fun2()

12. {

13. using namespace Utilities::operators;

14. unsigned long v = 1100_lb; // OK: littéral binaire long

15. }

Cette technique est proposée dans N3402.

III-F - Les littéraux de chaîne de caractères définis par l'utilisateur

Notez que les littéraux chaînes préparés (cooked) ne provoquent pas de collisions avec les littéraux numériques bruts, en raison du paramètre supplémentaire :

1. std::string operator"" _s(const char * str, unsigned len)

2. {

3. return std::string{str, len};

4. }

6. constexpr Seconds operator"" _s(const char * str)

7. {

8. return Seconds{str2int(str)};

9. }

10. int main()

11. {

12. std::string name = "dog"_s; // OK: string

13. Second elapsed  = 360_s;    // OK: Seconds

14. }

Le paramètre supplémentaire len est là non seulement pour éviter de telles ambiguïtés, mais aussi pour transmettre correctement la taille de la chaîne littérale dans le cas où elle contient le caractère '\0'. La taille d'une chaîne de caractères en langage C est déterminée par la première occurrence du caractère '\0' dans la séquence. Cependant, std::string stocke la taille de la chaîne séparément et '\0' est traité comme tout autre caractère :

1. std::string strange{ "hello\0""world", 11 };

2. assert (strange.length() == 11);

3. std::string strange2 = "hello\0""world"_s;

4. assert (strange2.length() == 11);

Notez que si nous avions défini notre chaîne littérale sans le paramètre len, nous obtiendrions une taille incorrecte :

1. std::string operator"" _badstr(const char * str, unsigned len)

2. {

3. return std::string{str};

4. }

5. std::string badstr = "hello\0""world"_badstr;

6. assert (badstr.length() == 5);

Le code ci-dessus devrait nous donner déjà une idée de ce que « préparer » (cooking) veut dire dans le cas des littéraux de chaîne « préparés ». Un autre exemple :

1. std::string text = R"dog(cat)dog"_s;

2. assert (text == "cat");

Si cette syntaxe « R » vous semble trop confuse, permettez-moi de vous expliquer juste ce que c'est un littéral de chaîne de caractères brut. La partie R"dog( indique que tout ce que nous analysons par la suite (y compris barre oblique inverse, guillemets doubles et parenthèses) est considéré comme un caractère quelconque jusqu'à ce qu'on rencontre la séquence finale )dog". Voici un autre exemple, plus simple :

1. std::string text = "d""o""g"_s;

2. assert (text.length() == 3);

3. text = "\"\"\""_s;

4. assert (text.length() == 3);

Cela nous rapproche de la réponse à la question pourquoi n'est-il pas possible de définir des opérateurs littéraux bruts de chaîne de caractères. Oui, vous ne pouvez pas les définir en C++11. Ils auraient été très utiles pour permettre des calculs sur des chaînes à la compilation, et l'on a même proposé leur ajout. Mais finalement, nous ne les avons pas. L'une des raisons est que la signification de « brut » n'est pas claire dans le cas des chaînes de caractères. Notez que dans le cas des opérateurs littéraux intégraux bruts, nous analysons aussi les préfixes :

1. 0x11_b;

2. // ce qui équivaut à :

3. operator"" _b<'0', 'x', '1', '1'>();

Quelle serait la signification du littéral "g""o"_s ?

1. operator"" _s<'g', 'o'>();                          // ??

2. operator"" _s<'\"', 'g', '\"', '\"', 'o', '\"'>();  // ??

Qu'en est-il des autres cas particuliers ?

1. "\?"_s;

2. operator"" _s<'\?'>();        // ??

3. operator"" _s<'\\', '\?'>();  // ??

4. R"(A)"_s;

5. operator"" _s<'R', '\"', '(', 'A', ')', '\"'>();  // ??

6. operator"" _s<'A'>();                             // ??

7. "\101"_s; // lettre "A"

8. operator"" _s<'A'>();                                    // ??

9. operator"" _s<'\\', '1', '0', '1'>();                    // ??

10. operator"" _s<'\"', '\\', '1', '0', '1', '\"'>();        // ??

11. operator"" _s<'\"', '\\', '1', '0', '1', '\"', '\0'>();  // ??

III-G - Repousser les limites

Dans ce billet et le précédent, nous avons vu comment nous pouvons analyser des littéraux entiers binaires. D'une manière similaire, nous pouvons analyser des entiers en base 3 : nous vérifions si chaque chiffre est 0, 1 ou 2. Pourrions-nous analyser des entiers en base 12 ? Puisque nous pouvons déjà analyser des entiers en base 16, la base 12 ne devrait poser aucun problème. En effet, nous pouvons le faire ; pourtant, puisque nous devons nous conformer à la syntaxe du C++, nous devons utiliser le préfixe 0x pour les littéraux hexadécimaux :

1. unsigned int i = 0x1A0_b12; // = 264

2. unsigned int j = 0x1C0_b12; // ERREUR: 'C' n'est pas un chiffre en base-12

Pouvons-nous analyser des entiers littéraux en base 32 ? Il semble que nous ne pouvons pas : nous pouvons juste restreindre la syntaxe des littéraux intégrés, mais nous ne pouvons pas l'étendre.

En plus des différentes bases, nous pouvons faire d'autres choses futées - peut-être trop futées. Par exemple, vous pouvez définir un littéral binaire qui utilise les chiffres 'A' et 'B' :

1. unsigned int i = 0xAABBABBA_bb;

Ce n'est pas particulièrement utile (et serait probablement source de confusion), mais cela vous donne une idée de ce qu'on peut faire avec les opérateurs littéraux. Vous pouvez également limiter les littéraux binaires uniquement à ceux ayant une certaine taille fixe :

1. unsigned int i = 0x11001001_bL8;  // OK: 201

2. unsigned int j = 0x11001010_bL8;  // OK: 202

3. unsigned int k = 0x1100100_bL8;   // ERREUR: trop court

4. unsigned int l = 0x110010010_bL8; // ERREUR: trop long

Vous pouvez aussi ruser et utiliser le point décimal dans le littéral à virgule flottante pour désigner des paires de nombres :

1. Hour t1 = 14.45_hour;  // 2:45 p.m.

2. Hour t2 =  2.45_hour;  // 2:45 a.m.

3. Hour x1 = 14.450_hour; // ERREUR: seulement deux chiffres autorisés après le point

4. Hour x2 =  0.61_hour;  // ERREUR: '6' non autorisé immédiatement après le point

Encore une fois, cela pourrait être une mauvaise idée, parce que les gens ont l'habitude d'utiliser le point pour indiquer un nombre à virgule flottante. Mais vous voyez les possibilités.

Vous pouvez juger par vous-même si les littéraux définis par l'utilisateur sont utiles et s'ils méritent leur place dans la norme C++.

III-H - Remerciements

Toute l'équipe de Developpez.com remercie sincèrement Andrzej Krzemieński qui nous a aimablement permis de traduire et de publier son tutoriel sur notre site. Nous remercions aussi Mishulyna d'avoir fait la traduction, Francis Walter et Loïc Joly pour leur relecture technique et Claude Leloup pour les corrections orthographiques.

Vous pouvez retrouver l'ensemble des traductions de Andrzej Krzemieński qui ont été faites par l'équipe des traducteurs bénévoles de Developpez.com ici : Blog de Andrzej Krzemieński.

N'hésitez pas à donner votre avis sur cet billet sur notre forum : Commentez .

(1)En réalité il s'agit de la masse car le poids est exprimé en newtons tandis que la masse en Kg. Pour une cohérence, nous avons conservé le terme poids que l'auteur a utilisé dans l'article original.

(2)Avec le standard C++14, cette implémentation est beaucoup plus simple à faire

(3)Depuis l'écriture de l'article, le C++ a évolué et propose nativement des littéraux binaires, notés 0b010110.