User Defined Literals en C++
Tags: cpp, programming
Published: 2020-10-31 09:42:06

Les [_user defined literals_](https://en.cppreference.com/w/cpp/language/user_literal) ne sont pas une fonctionnalité 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 :

```cpp
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;
};
```

Cette classe est volontairement basique mais elle est suffisante pour cet article. Voici un exemple de création et d'utilisation :

```cpp
Distance a{11.26};
std::cout << a << '\n';
```

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 :

```cpp
auto marathon = Distance(42.195 * 1000);
auto micrometer = Distance(1e-6);
auto feet = Distance(0.3048);
```

Comment pourrait-on créer des `Distance`s à 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 `double` 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 `Distance::fromFoot()` ou `Distance::fromKilometers()`.

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](https://en.cppreference.com/w/cpp/language/user_literal) :

> 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 `42_km`, `1_um`, `0.3048_ft`. Ca a l'air cool, hein ?

## Comment créer des _user defined literals_ pour un type ?

Il suffit d'implémenter des `operator""()` pour supporter les suffixes de votre choix.

Voici par exemple les opérateurs à implementer pour supporter plusieurs suffixes, et ainsi créer des `Distance`s à partir de valeurs dans différentes unités :

```cpp
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);
}
```

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 :

> `error: 'Distance Distance::operator""_km(long double)' must be a non-member function`

Voici un exemple d'utilisation :

```cpp
std::cout << 42.195_km;
std::cout << 1.1_ft;
std::cout << 1.0_um;
```

Euh ? On ne peut pas simplement écrire `1_um` ? 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) :

> `error: unable to find numeric literal operator 'operator""_um'`

Pour comprendre pourquoi, je vais d'abord expliquer pourquoi j'ai mis des `long double`s en paramètre de mes opérateurs, alors que des `double`s semblent suffisants puisque le constructeur de la classe `Distance` 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* :

> `( 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 )`

Le type le plus proche de `double` est bien `long double`.

Le type du _literal_ avant le suffixe est utilisé par le compilateur pour trouver le bon opérateur. Ainsi, `1.0_um` va trouver ma surcharge avec `long double` mais `1_um` ne trouvera pas la surcharge prenant en paramètre `unsigned long long int` 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 `std::cout << -12.0_m << '\n';` génère l'erreur  `error: no match for 'operator-' (operand type is 'Distance')`. Il suffit d'ajouter un tel opérateur à la classe :

```cpp
Distance operator-() {
	return Distance(-meters_m);
}
```

Le tour est joué : `std::cout << -12.0_m << '\n';` compile désormais et affiche bien `-12000 m`.

## 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_](https://github.com/nholthaus/units), créée par [Nic Holthaus](https://github.com/nholthaus). 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 :

```cpp
#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';
}
```

A chaque étape, il y a des vérifications de types qui empêchent d'écrire du code avec des erreurs de dimensions, tel que `square_meter_t width = 1_m;`, 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 `Distance`, `Voltage` et `IPAddress`.

Pour créer un objet de type `Foo` à partir d'un _literal_ de type `literal_type` et du suffixe `_suffix`, il faut implémenter un _literal operator_ de la forme `Foo operator ""_suffix(literal_type)`.

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](https://internals.rust-lang.org/t/pre-rfc-custom-literals-via-traits), [C#](https://github.com/dotnet/roslyn/issues/263) ou encore [Python](https://mail.python.org/pipermail/python-ideas/2015-June/033871.html), mais elles ne semblent pas avoir abouties.