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

Les littéraux utilisateur


précédentsommaire

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
template <bool COND>
struct Bool2Type_
{
  using type = int;                                 // (1)
};
template <>
struct Bool2Type_<true>
{
  using type = std::string;
};
template <bool COND>
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 :

 
Sélectionnez
1.
2.
3.
// non autorisé en C++, mais vous comprenez ce que cela veut dire
Bool2Type<false> == int;
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
template <bool COND>
Bool2Type<COND> bool2val()
{
  return 0;
}
template<>
Bool2Type<true> bool2val<true>()
{
  return std::string{"one"};
}
int main()
{
  assert (bool2val<false>() == 0);
  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 :

 
Sélectionnez
1.
2.
3.
4.
5.
using T0 = typename std::conditional<false, std::string, int>::type;
using T1 = typename std::conditional<true, std::string, int>::type;
// non C++:
T0 == int;
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 :

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

 
Sélectionnez
1.
2.
3.
4.
template <bool COND, typename T, typename F>
using IF = typename std::conditional<COND, T, F>::type;
// utilisation :
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
if (SIZE > sizeof(long long unsigned) * 8) {
  ERROR();
}
if (SIZE <= sizeof(unsigned) * 8) {
  return <unsigned>;
}
else {
  if (SIZE <= sizeof(long unsigned) * 8) {
    return <long unsigned>;
  }
  else {
    return <long long unsigned>;
  }
}

Nous pouvons l'implémenter comme ceci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
template <size_t SIZE>
struct select_type
{
  template <typename T>
  constexpr size_t NumberOfBits()
  {
    return std::numeric_limits<T>::digits;
  }
 
  static_assert(SIZE <= NumberOfBits<long long unsigned>(), "littéral binaire trop long");
   
  using type = IF<(SIZE <= NumberOfBits<unsigned>()), 
    unsigned, 
    IF<(SIZE <= NumberOfBits<long unsigned>()), 
      long unsigned,
      long long unsigned
    >
  >;
};
template <size_t SIZE>
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 :

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
constexpr bool is_binary( char c )
{
  return c == '0' || c == '1';
}
template <typename UINT, UINT VAL>
constexpr UINT build_binary_literal()
{
  static_assert(std::is_unsigned<UINT>::value, "exige un type unsigned");
  return VAL;
}
template <typename UINT, UINT VAL, char DIGIT, char... REST>
constexpr UINT build_binary_literal()
{
  static_assert(is_binary(DIGIT), "seuls les 0 et les 1 sont permis");
  static_assert(std::is_unsigned<UINT>::value, "exige un type unsigned");
  return build_binary_literal<UINT, 2 * VAL + DIGIT - '0', REST...>();
}
template <char... STR>
constexpr SelectType<sizeof...(STR)> operator"" _b()
{
  return build_binary_literal<SelectType<sizeof...(STR)>, 0, STR...>();
}

Maintenant, nous pouvons tester notre littéral :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
static_assert(0_b == 0, "!!");
static_assert(1_b == 1, "!!");
static_assert(10_b == 2, "!!");
static_assert(1000100010001000100010001000100010001000_b == 0x8888888888, "!!");
int main()
{
  auto i = 10001000100010001000100010001000_b;
  auto j = 1000100010001000100010001000100010_b;
  static_assert( std::is_same<decltype(i), unsigned int>::value, "!unsigned" );
  static_assert( std::is_same<decltype(j), unsigned long long>::value, "!ull" );
}

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// units_library.hpp:
namespace Units
{
  class Mass;
  Mass operator"" _lb(long double);
  Mass operator"" _kg(long double);
}
using Units::operator"" _lb;
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 :

 
Sélectionnez
1.
2.
3.
// utilities.hpp:
long operator"" _b(const char *);           // littéral binaire
unsigned long operator"" _lb(const char *); // littéral binaire long
 
Sélectionnez
1.
2.
3.
4.
// main.cpp:
# include "units_library.hpp"
# include "utilities.hpp"
// 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
// units_library2.hpp:
namespace Units
{
  class Mass;
  namespace operators
  {
    Mass operator"" _lb(long double);
    Mass operator"" _kg(long double);
  }
}
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
// main.cpp:
# include "units_library2.hpp"
# include "utilities.hpp"
// OK jusqu'ici
Units::Mass m1 = 200_kg; // OK
Units::Mass m2 = 400_lb; // ERREUR: ambiguïté

Cette technique est proposée dans N2750N2750 (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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
// units_library3.hpp:
namespace Units
{
  class Mass;
  namespace operators
  {
    Mass operator"" _lb(long double);
    Mass operator"" _kg(long double);
  }
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// utilities_b.hpp:
namespace Utilities
{
  namespace operators
  {
    long operator"" _b(const char *);           // littéral binaire
    unsigned long operator"" _lb(const char *); // littéral binaire long
  }
}
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// main.cpp:
 
# include "units_library3.hpp"
# include "utilities_b.hpp"
void fun1()
{
  using namespace Units::operators;
  Units::Mass m1 = 2100_kg; // OK
  Units::Mass m2 = 1100_lb; // OK
}
void fun2()
{
  using namespace Utilities::operators;
  unsigned long v = 1100_lb; // OK: littéral binaire long
}

Cette technique est proposée dans N3402N3402.

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
std::string operator"" _s(const char * str, unsigned len) 
{ 
  return std::string{str, len};
}
 
constexpr Seconds operator"" _s(const char * str) 
{
  return Seconds{str2int(str)};
}
int main()
{
  std::string name = "dog"_s; // OK: string
  Second elapsed  = 360_s;    // OK: Seconds
}

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 :

 
Sélectionnez
1.
2.
3.
4.
std::string strange{ "hello\0""world", 11 };
assert (strange.length() == 11);
std::string strange2 = "hello\0""world"_s;
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
std::string operator"" _badstr(const char * str, unsigned len)
{
  return std::string{str};
}
std::string badstr = "hello\0""world"_badstr;
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 :

 
Sélectionnez
1.
2.
std::string text = R"dog(cat)dog"_s;
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 :

 
Sélectionnez
1.
2.
3.
4.
std::string text = "d""o""g"_s;
assert (text.length() == 3);
text = "\"\"\""_s;
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 :

 
Sélectionnez
1.
2.
3.
0x11_b;
// ce qui équivaut à :
operator"" _b<'0', 'x', '1', '1'>();

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

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

Qu'en est-il des autres cas particuliers ?

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
"\?"_s;
operator"" _s<'\?'>();        // ??
operator"" _s<'\\', '\?'>();  // ??
R"(A)"_s;
operator"" _s<'R', '\"', '(', 'A', ')', '\"'>();  // ??
operator"" _s<'A'>();                             // ??
"\101"_s; // lettre "A"
operator"" _s<'A'>();                                    // ??
operator"" _s<'\\', '1', '0', '1'>();                    // ??
operator"" _s<'\"', '\\', '1', '0', '1', '\"'>();        // ??
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 :

 
Sélectionnez
1.
2.
unsigned int i = 0x1A0_b12; // = 264
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' :

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

 
Sélectionnez
1.
2.
3.
4.
unsigned int i = 0x11001001_bL8;  // OK: 201
unsigned int j = 0x11001010_bL8;  // OK: 202
unsigned int k = 0x1100100_bL8;   // ERREUR: trop court
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 :

 
Sélectionnez
1.
2.
3.
4.
Hour t1 = 14.45_hour;  // 2:45 p.m.
Hour t2 =  2.45_hour;  // 2:45 a.m.
Hour x1 = 14.450_hour; // ERREUR: seulement deux chiffres autorisés après le point
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 Donner une note à l´article (5).


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

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.