Littéraux maison

Depuis C++ 11, il est possible en C++ d'exprimer ce qu'on nomme des littéraux maison, soit des expressions qui apparaissent comme des littéraux mais qui sont évaluées par programmation. Ceci permet d'accroître de manière significative la robustesse des programmes en offrant aux programmeuses et aux programmeurs la possibilité d'exprimer leurs idées dans des termes qui se rapprochent de leur domaine d'application.

Par exemple, plutôt que d'écrire quelque chose comme :

#include <chrono>
using namespace std::chrono;
// ...
auto delai = hours{3} + minutes{15} + seconds {30};

...il devient possible d'exprimer la même idée comme suit (notez que les littéraux utilisés ici sont standards à partir de C++ 14) :

#include <chrono>
using namespace std::chrono;
// ...
auto delai = 3h + 15min + 30s;

Dans cet exemple, les littéraux portant comme suffixe h, m et s sont définis dans l'espace nommé std::chrono.

Les règles du jeu

Les règles du jeu pour définir un littéral maison sont les suivantes :

Quelques exemples légaux suivent :

Signature Exemples de littéraux
Kilometre operator"" _Km(unsigned long long);
3_Km
123_Km
-15_Km // appelle operator-(Kilometre)
Angle operator"" _rad(long double);
3.14159_rad
char operator"" _majuscule(char);
'a'_majuscule
'Z'_majuscule
'3'_majuscule
std::string operator"" _str(const char*,std::size_t);
"J'aime mon prof"_str
R"(Ceci est
un littéral sur
plusieurs lignes)"_str

Exemple : littéral factoriel

Un exemple simple (et plus académique que sérieux) de recours à un littéral maison serait le suivant :

Nous pourrions alors écrire ce qui suit :

#include <vector>
#include <iostream>
using namespace std;
constexpr long long factorielle(int n)
   { return n == 0 || n == 1 ? 1 : n * factorielle(n-1); }
constexpr long long operator "" _fac(unsigned long long n)
   { return factorielle(static_cast<int>(n)); }
int main() {
   cout << 5_fac << endl;
}

Ici, 5_fac sera évalué à la compilation et résolu comme une constante, et pourra donc être utilisé entre autres pour déterminer la taille d'un tableau alloué automatiquement. Notez le static_cast<int> dans l'implémentation de l'opérateur, pour relayer l'unsigned long long reçu par l'opérateur vers la fonction factorielle(int); ce transtypage nous épargnera un avertissement.

J'ai pris cette idée d'un courriel d'Andrew Tomazos en 2016.

Exemple : littéral fichier

Une autre application amusante serait un littéral fichier, qui construirait une représentation en mémoire du contenu d'un fichier binaire ou texte.

J'ai commencé par construire une classe représentant la contrepartie en mémoire d'un fichier, étant donné son nom (suppléé à la construction).

Mon objectif ici était d'offrir une représentation mémoire efficace en temps, donc je n'ai pas cherché à minimiser les coûts en termes de consommation d'espace mémoire.

Pour couvrir à la fois les fichiers binaires et les fichiers textes, j'ai représenté les données comme un vector<char>, donc comme une séquence de bytes bruts. L'idée est que dans le cas d'un fichier texte, l'effort de « conversion » requis sera minimal. Notez que je n'ai pas cherché ici à couvrir diverses formes d'encodage, donc je n'offre que des opérateurs explicites de conversion en vector<char> (ce qui, en pratique, est plus une copie qu'une conversion, quoique j'aurais pu faire mieux encore ici) ou en string.

#ifndef FILE_REPRESENTATION_H
#define FILE_REPRESENTATION_H
#include <fstream>
#include <vector>
#include <string>
#include <iterator>
class file_representation {
   std::vector<char> raw;
   static std::vector<char> read_contents(const std::string &s) {
      std::ifstream is{ s, std::ios::binary };
      return{ std::istreambuf_iterator<char>{is}, std::istreambuf_iterator<char>{} };
   }
public:
   file_representation(const std::string &s) : raw{ read_contents(s) } {
   }
   explicit operator std::string() const {
      return{ begin(raw), end(raw) };
   }
   explicit operator std::vector<char>() const {
      return raw;
   }
};

Le littéral maison de suffixe _file n'aura pour seul rôle que celui de retrourner une représentation en mémoire du fichier indiqué.

file_representation operator "" _file(const char *s, std::size_t n) {
   return{ std::string{ s, s + n } };
}
#endif

Étant donné ces (petits!) outils, le programme principal proposé à droite suffit pour lire et afficher à la console le contenu d'un fichier texte étant donné son nom.

#include "file_representation.h"
#include <iostream>
int main() {
   using namespace std;
   cout << static_cast<string>("Principal.cpp"_file) << endl;
}

C'est charmant, il me semble.

Exemple : un littéral constexpr variadique

Il est possible de rédiger des littéraux arbitrairement longs à l'aide de templates variadiques sur des caractères. Cette approche (quelque peu laborieuse) a plusieurs applications intéressantes, dont des hash ou des CRC calculés à la compilation; l'exemple qui suit est beaucoup plus humble et se limite à calculer le nombre de caractères qui représentent un entier pair (donc '0', '2', '4', '6' et '8') dans un littéral donné. Vous saurez extrapoler des réalisations plus pertinentes sur la base des mêmes techniques.

Le code suit :

// ...
template <char ...> struct count_even;
template <char C> struct count_even<C> {
    enum { value = (C - '0') % 2 == 0? 1 : 0 };
};
template <char C, char ...Cs> struct count_even<C, Cs...>
   : count_even<Cs...>
{
    enum { value = ((C - '0') % 2 == 0? 1 : 0) + count_even::value };
};
template <char ... Cs>
   constexpr long long operator "" _npairs() {
      return count_even<Cs...>::value;
   }
int main() {
    cout << 1234567_npairs << endl;
}

Utilité

L'idée derrière les littéraux maison est de permettre, dans un programme, l'expression plus directe de concepts du domaine d'application. Par exemple, en C ou en C++ d'une autre époque, il était fréquent de rencontrer du code comme ceci :

#define DEG_2_RAD(theta) ((theta)*0.0174532925)
// ...
int main() {
   double angle_degres;
   if (cin >> angle_degres) {
      double angle_radians = DEG_2_RAD(angle_degres);
      // ...
   }
}

Évidemment, les macros échappant au compilateur, ce code est plutôt à risque (il suffit d'oublier les parenthèses autour de theta dans la macro DEG_2_RAD et d'y utiliser une expression ayant des effets de bord pour que le plaisir commence...).

En C++ plus contemporain, il est possible d'exprimer des conversions de référentiels guidées par les types, pour atteindre le même niveau d'efficacité qu'avec une macro (en fait, souvent mieux!) mais de manière plus sécuritaire.

Par exemple :

class Radians{};
class Degres{};
template <class>
   class Angle {
      // ...
   };
// ...
int main() {
   Angle<Degres> theta;
   if (cin >> angle_degres) {
      Angle<Radians> theta_prime = theta; // constructeur de conversion
      // ...
   }
}

L'ajout de littéraux maison ne fait qu'offrir un nouveau moyen d'exprimer, de manière concise, certaines entités dont les valeurs sont connues a priori. Les littéraux maison ne remplacent pas du code comme celui ci-dessus, où les valeurs sont découvertes dynamiquement, mais peuvent permettre d'exprimer des programmes comme celui-ci :

class Radians{};
class Degres{};
template <class>
   class Angle {
      // ...
   };
// ...
Angle<Degres> operator"" _deg(long double);
Angle<Radians> operator"" _rad(long double);
// ...
void f() {
   // Ceci demeure tout à fait correct
   auto theta = Angle<Degres>{180.0};
   auto theta_prime = Angle<Radians>{3.14159};
   // ...
}
void g() {
   // Ceci est une nouvelle option
   auto theta = 180.0_deg
   auto theta_prime = 3.14159_rad;
   // ...
}

Dans la mesure où les abus sont évités, ceci peut ajouter à l'expressivité des programmes.

Saines pratiques

Les saines pratiques avec les littéraux maison ne sont pas encore pleinement connues, mais on peut supposer qu'elles incluront au moins ce qui suit :

Littéraux maisons et standard C++ 14

Les littéraux maison standards de C++ 14 sont les suivants :

Suffixe Type
s
std::string // pour const char*,size_t
std::chrono::seconds // pour unsigned long long
h
std::chrono::hours
min
std::chrono::minutes
ms
std::chrono::milliseconds
us
std::chrono::microseconds
ns
std::chrono::nanoseconds
il
std::complex<long double>
i
std::complex<double>
if
std::complex<float>

Vous comprendrez qu'accepter le suffixe if, qui est aussi un mot clé, a demandé une adaptation grammaticale pour que operator"" if(float) soit une écriture acceptable.

Avec ces littéraux, une expression comme 3.0+5i est tout à fait raisonnable désormais et exprime un complex<double>, comme on pourrait raisonnablement s'y attendre.

Lectures complémentaires


Valid XHTML 1.0 Transitional

CSS Valide !