Les user defined literals ne sont pas une feature récente, ils sont disponibles depuis C++11. Pourtant, j'ai constaté récemment qu'ils n'étaient clairement pas connus de tous. C'est l'occasion de faire un article pour être certain que vous aussi, vous connaissiez leur existence !
Point de départ : un type pour stocker une distance
Pour illustrer l'utilité des user defined literals, prenons comme fil rouge un type permettant de stocker une distance :
<pre><code>class Distance {
public:
explicit Distance(double meters) :
meters_m(meters) {}
friend std::ostream& operator<<(std::ostream& os, const Distance& distance) {
os << distance.meters_m << " m";
return os;
}
private:
double meters_m = 0;
};<code><pre>
Cette classe est volontairement basique mais elle est suffisante pour cet article. Voici un exemple de création et d'utilisation :
<pre><code>Distance a{11.26};
std::cout << a << '\n';<code><pre>
Très bien, donc ?
Oui et non... Oui, parce qu'elle fait ce qu'on lui demande. Non, parce que son utilisation n'est pas forcément simple. Si vous avez une distance en mètres, c'est facile d'appeler le constructeur puisque c'est ce qu'il attend. Mais si vous avez des distances dans d'autres unités, les valeurs à passer au constructeur peuvent être un peu compliquées à lire :
<pre><code>auto marathon = Distance(42.195 * 1000);
auto micrometer = Distance(1e-6);
auto feet = Distance(0.3048);<code><pre>
Comment pourrait-on créer des <span class="css-span">Distances</span> à partir de valeurs dans une autre unité que le mètre ?
Il n'est pas possible d'avoir plusieurs constructeurs car on ne pourrait pas faire d'overloading avec un seul paramètre. Pour rappel, le nom du paramètre n'a aucun effet, seul son type importe. Il est donc impossible d'avoir plusieurs constructeurs prenant chacun en paramètre un <span class="css-span">double</span> mais dans des unités différentes.
On pourrait imaginer des techniques plus ou moins pratiques :
- un enum à passer en deuxième paramètre au constructeur pour préciser l'unité de la valeur,
- des fonctions statiques type "builder" comme <span class="css-span">Distance::fromFoot()</span> ou <span class="css-span">Distance::fromKilometers()</span>.
Mais je vous le donne en mille, il y a une autre solution en C++ : les user defined literals.
C'est quoi en fait un user defined literal ?
La définition d'un user defined literal est sur cppreference :
Allows integer, floating-point, character, and string literals to produce objects of user-defined type by defining a user-defined suffix.
C'est donc une possibilité d'écrire quelque chose comme <span class="css-span">42_km, 1_um, 0.3048_ft</span>. Ca a l'air cool, hein ?
Comment créer des user defined literals pour un type ?
Il suffit d'implémenter des <span class="css-span">operator""()</span> pour supporter les suffixes de votre choix.
Voici par exemple les opérateurs à implementer pour supporter plusieurs suffixes, et ainsi créer des <span class="css-span">Distances</span> à partir de valeurs dans différentes unités :
<pre><code>Distance operator ""_km(long double value) {
return Distance(value * 1e3);
}
Distance operator ""_m(long double value) {
return Distance(value);
}
Distance operator ""_um(long double value) {
return Distance(value * 1e-6);
}
Distance operator ""_ft(long double value) {
return Distance(value * 0.3048);
}<code><pre>
Ces opérateurs ne peuvent pas être définis à l'intérieur de la classe, ils doivent être libres. Vous pouvez essayer mais votre compilateur devrait vous rappeler à l'ordre, comme par exemple avec gcc :
<span class="css-span">error: 'Distance Distance::operator""_km(long double)' must be a non-member function</span>
Voici un exemple d'utilisation :
<pre><code>std::cout << 42.195_km;
std::cout << 1.1_ft;
std::cout << 1.0_um;<code><pre>
Euh ? On ne peut pas simplement écrire <span class="css-span">1_um </span>? Avec les opérateurs présentés ci-dessus : non. L'erreur émise par gcc est la suivante (et elle n'est pas super claire de prime abord) :
<span class="css-span">error: unable to find numeric literal operator 'operator""_um'</span>
Pour comprendre pourquoi, je vais d'abord expliquer pourquoi j'ai mis des <span class="css-span">long doubles</span> en paramètre de mes opérateurs, alors que des <span class="css-span">doubles</span> semblent suffisants puisque le constructeur de la classe <span class="css-span">Distance</span> attend un double paramètre. La raison est simple : les types de paramètres possibles pour ces opérateurs ne sont pas libres. La liste des possibles est fournie sur cppreference dans la section Literal operators :
<span class="css-span">( const char * )
( unsigned long long int )
( long double )
( char )
( wchar_t )
( char8_t ) (since C++20)
( char16_t )
( char32_t )
( const char * , std::size_t )
( const wchar_t * , std::size_t )
( const char8_t * , std::size_t ) (since C++20)
( const char16_t * , std::size_t )
( const char32_t * , std::size_t )</span>
Le type le plus proche de <span class="css-span">double</span> est bien <span class="css-span">long double</span>.
Le type du literal avant le suffixe est utilisé par le compilateur pour trouver le bon opérateur. Ainsi, <span class="css-span">1.0_um</span> va trouver ma surcharge avec <span class="css-span">long double</span> mais <span class="css-span">1_um</span> ne trouvera pas la surcharge prenant en paramètre <span class="css-span">unsigned long long int</span> et donc ne compilera pas. Il suffit de l'implémenter pour que le code fonctionne.
Peut-on avoir des user defined literals négatifs ?
Stricto sensu, on ne peut pas avoir de user defined literals négatifs, mais il y a en fait une astuce pour y arriver. Il suffit d'essayer pour que le compilateur nous donne la technique. Par exemple <span class="css-span">std::cout << -12.0_m << '\n';</span> génère l'erreur <span class="css-span">error: no match for 'operator-' (operand type is 'Distance')</span>. Il suffit d'ajouter un tel opérateur à la classe :
<pre><code>Distance operator-() {
return Distance(-meters_m);
}<code><pre>
Le tour est joué : <span class="css-span">std::cout << -12.0_m << '\n';</span> compile désormais et affiche bien <span class="css-span">-12000 m</span>.
Exemple plus avancé : la bibliothèque Units
Si vous voulez vraiment gérer des distances (ou d'autres dimensions physiques avec plein d'unités), inutile de coder ça vous-même. Je vous propose d'aller faire un tour sur Github et de trouver une bonne bibliothèque. J'ai notamment testé et bien aimé UNITS, créée par Nic Holthaus. Je vous parle de celle-ci en particulier car elle fait une utilisation massive de user defined literals. Ecrite en C++14, cette bibliothèque est single header et est donc facile à intégrer à votre projet. Voici un exemple d'utilisation :
<pre><code>#include "units.h"
int main() {
using namespace units::area;
using namespace units::length;
using namespace units::volume;
using namespace units::literals;
meter_t width = 15_m;
std::cout << width << '\n';
meter_t height = 2.50_ft;
std::cout << height << '\n';
square_meter_t area = width * height;
std::cout << area << '\n';
cubic_meter_t volume = area * 1_m;
std::cout << volume << '\n';
std::cout << 2 * volume << '\n';}<code><pre>
À chaque étape, il y a des vérifications de types qui empêchent d'écrire du code avec des erreurs de dimensions, tel que <span class="css-span">square_meter_t width = 1_m;</span>, qui provoque une erreur de compilation.
Conclusion
Les user defined literals permettent de créer des objets à partir d'un literal (integer, floating-point, character ou string) et de user-defined suffixes. On peut ainsi écrire des constantes telles ques 42.195_km, 3.3_Volts ou encore "192.168.0.1"_IPV4, très probablement pour obtenir des objets de type <span class="css-span">Distance</span>, <span class="css-span">Voltage</span> et <span class="css-span">IPAddress</span>.
Pour créer un objet de type <span class="css-span">Foo</span> à partir d'un literal de type <span class="css-span">literal_type</span> et du suffixe <span class="css-span">_suffix</span>, il faut implémenter un literal operator de la forme Foo operator <span class="css-span">""_suffix(literal_type)</span>.
Cette fonctionnalité semble particulière au C++. Il y a visiblement des propositions pour intégrer des features semblables dans d'autres langages, tels Rust, C# ou encore Python, mais elles ne semblent pas avoir abouti.
Que la vie de Pierre, expert embarqué Younup, serait terne sans les variadic templates et les fold expressions de C++17. Heureusement pour lui, Python a tué l'éternel débat sur l’emplacement de l’accolade : "alors, à la fin de la ligne courante ou au début de la ligne suivante ?"
Homme de terrain, il est aussi à l’aise au guidon de son VTT à sillonner les chemins de forêt, dans une salle de concert de black metal ou les mains dans les soudures de sa carte électronique quand il doit déboguer du code (bon ça, il aime moins quand même !)
Son vœu pieux ? Il hésite encore... Faire disparaitre le C embarqué au profit du C++ embarqué ? Ou stopper la génération sans fin d'entropie de son bureau.