IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Les littéraux utilisateur


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

 
Sélectionnez
auto i = 101_b;

Il le traite comme :

 
Sélectionnez
auto i = operator"" _b("101");

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

 
Sélectionnez
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 :

 
Sélectionnez
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++ ;

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 à :

 
Sélectionnez
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 :

 
Sélectionnez
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) :

 
Sélectionnez
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.


précédentsommairesuivant
Avec le standard C++14, cette implémentation est beaucoup plus simple à faire

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 ni 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.