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
(11
UL) ==
unsigned
long
;
decltype
(11
LL) ==
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(12
U); // 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.