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 :
- Nous autorisons seulement des 0 et des 1 dans chaque caractère du littéral ;
- À 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.