Les littéraux utilisateur

Partie 1 : les littéraux préparés

Cet article est le premier d'une série de trois au sujet d'une nouveauté du langage : la possibilité de définir des littéraux utilisateur.

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.

Vous pouvez réagir au contenu de ce tutoriel sur le forum C++ : 8 commentaires Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Site personnel

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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

 
Sélectionnez
int= 0;
i = 17;

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

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

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

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

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

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

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

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

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

III. 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 :

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

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

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

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

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

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

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

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

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

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

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

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

IV. 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) :

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

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

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

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

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

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

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

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

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   


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.

  

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.