Nous mettons les lignes dans les paramètres du modèle

Le C ++ moderne nous a apporté un tas de fonctionnalités qui faisaient cruellement défaut dans le langage. Afin d'obtenir en quelque sorte un effet similaire, de superbes béquilles ont été inventées pendant longtemps, principalement composées de très grands couvre-pieds de motifs et de macros (souvent également autogènes). Mais maintenant, de temps en temps, un besoin se fait sentir d'opportunités qui ne sont pas encore dans la langue. Et nous recommençons à réinventer des conceptions complexes à partir de modèles et de macros, à les générer et à obtenir le comportement dont nous avons besoin. C'est juste une telle histoire.

Au cours du dernier semestre, j'ai eu deux fois besoin de valeurs pouvant être utilisées dans les paramètres du modèle. Dans le même temps, je voulais avoir des noms lisibles par l'homme pour ces valeurs et exclure la nécessité de déclarer ces noms à l'avance. Les tâches spécifiques que j'ai résolues étaient un problème distinct, peut-être plus tard j'écrirai des articles séparés à leur sujet, quelque part dans le hub de «programmation anormale». Je vais maintenant parler de l'approche par laquelle j'ai résolu ce problème.

Ainsi, en ce qui concerne les paramètres du modèle, nous pouvons utiliser soit le type soit la valeur de constante statique. Pour la plupart des tâches, c'est plus que suffisant. Nous voulons utiliser des identificateurs lisibles par l'homme dans les paramètres - nous déclarons la structure, l'énumération ou la constante et les utilisons. Les problèmes commencent lorsque nous ne pouvons pas déterminer cet identifiant à l'avance et que nous voulons le faire sur place.

Il serait possible de déclarer une structure ou une classe directement dans le paramètre de modèle. Cela fonctionnera même si le modèle ne fait rien avec ce paramètre qui nécessite une description complète de la structure. De plus, nous ne pouvons pas contrôler l'espace de noms dans lequel une telle structure est déclarée. Et les substitutions de modèles d'apparence complètement identique se transformeront en un code complètement différent si ces lignes se trouvent dans des classes ou des espaces de noms voisins.

Vous devez utiliser des littéraux, et de tous les littéraux en C ++, seuls un littéral de caractère et un littéral de chaîne peuvent être appelés lisibles. Mais un littéral de caractères est limité à quatre caractères (lors de l'utilisation de char32_t), et un littéral de chaîne est un tableau de caractères et sa valeur ne peut pas être transmise aux paramètres du modèle.

Il se révèle une sorte de cercle vicieux. Vous devez soit déclarer quelque chose à l'avance, soit utiliser des identifiants gênants. Essayons d’obtenir la langue à laquelle elle n’est pas adaptée. Et si vous implémentiez une macro qui rendrait quelque chose approprié pour une utilisation dans les arguments de modèle à partir d'un littéral de chaîne?

Faisons la structure de la chaîne


Commençons par créer la base de la chaîne. En C ++ 11, des arguments de modèle variadic sont apparus.
Nous déclarons une structure qui contient des caractères de chaîne dans les arguments:

template <char... Chars> struct String{}; 

github

Ça marche. Nous pouvons même utiliser immédiatement ces lignes comme ceci:

 template <class T> struct Foo {}; Foo<String<'B', 'a', 'r'>> foo; 

Faites maintenant glisser cette ligne dans le runtime


Super. Il ne serait pas mauvais de pouvoir obtenir la valeur de cette chaîne lors de l'exécution. Soit une structure de modèle supplémentaire qui extraira les arguments d'une telle chaîne et en fera une constante:

 template <class T> struct Get; template <char... Chars> struct Get<String<Chars...>> { static constexpr char value[] = { Chars... }; }; 

Cela fonctionne également. Puisque nos lignes ne contiennent pas '\ 0' à la fin, vous devez soigneusement travailler avec cette constante (il est préférable, à mon avis, de créer immédiatement une chaîne_vue en utilisant la constante et la taille de celle-ci dans les arguments du constructeur). On pourrait simplement ajouter «\ 0» à la fin du tableau, mais ce n'est pas nécessaire pour mes tâches.

Vérifiez que nous pouvons manipuler de telles chaînes


D'accord, que pouvez-vous faire d'autre avec de telles cordes? Par exemple, concaténer:

 template <class A, class B> struct Concatenate; template <char... Chars, char... ExtraChars...> struct Concatenate<String<Chars...>, String<ExtraChars...>> { using type = String<Chars..., ExtraChars...>; }; 

github

En principe, vous pouvez faire plus ou moins n'importe quelle opération (je ne l'ai pas essayé, car je n'en ai pas besoin, mais je ne peux qu'imaginer comment vous pouvez rechercher une sous-chaîne ou même remplacer une sous-chaîne).
Nous avons maintenant la question principale, comment extraire des caractères d'un littéral de chaîne au moment de la compilation et les mettre dans les arguments du modèle.

Dessinez le hibou et écrivez une macro.


Commençons par un moyen de mettre les caractères dans les arguments du modèle un par un:

 template <class T, char c> struct PushBackCharacter; template <char... Chars, char c> struct PushBackCharacter<String<Chars...>, c> { using type = String<Chars..., c>; }; template <char... Chars> struct PushBackCharacter<String<Chars...>, '\0'> { using type = String<Chars...>; }; 

github

J'utilise une spécialisation distincte pour le caractère '\ 0', afin de ne pas l'ajouter à la chaîne utilisée. En outre, cela simplifie quelque peu les autres parties de la macro.

La bonne nouvelle est qu'un littéral de chaîne peut être un paramètre de la fonction constexpr. Nous allons écrire une fonction qui retournera un caractère par index dans une chaîne ou '\ 0' si la chaîne est plus courte que l'index (ici la spécialisation PushBackCharacter pour le caractère '\ 0' est très pratique).

 template <size_t N> constexpr char CharAt(const char (&s)[N], size_t i) { return i < N ? s[i] : '\0'; } 

Fondamentalement, nous pouvons déjà écrire quelque chose comme ça:

 PushBackCharacter< PushBackCharacter< PushBackCharacter< PushBackCharacter< String<>, CharAt("foo", 0) >::type, CharAt("foo", 1) >::type, CharAt("foo", 2) >::type, CharAt("foo", 3) >::type 

Nous avons mis un tel footcloth, mais plus authentique (nous pouvons écrire des scripts pour générer du code) à l'intérieur de notre macro, et c'est tout!

Il y a une nuance. Si le nombre de caractères dans la ligne est supérieur aux niveaux d'imbrication dans la macro, la ligne sera simplement coupée et nous ne le remarquerons même pas. Le désordre.

Faisons une structure de plus, qui ne convertit en aucun cas la chaîne qui y parvient, mais fait static_assert pour que sa longueur ne dépasse pas une constante:

 #define _NUMBER_TO_STR(n) #n #define NUMBER_TO_STR(n) _NUMBER_TO_STR(n) template <class String, size_t size> struct LiteralSizeLimiter { using type = String; static_assert(size <= MAX_META_STRING_LITERAL_SIZE, "at most " NUMBER_TO_STR(MAX_META_STRING_LITERAL_SIZE) " characters allowed for constexpr string literal"); }; #undef NUMBER_TO_STR #undef _NUMBER_TO_STR 

Eh bien, la macro ressemblera à ceci:

 #define MAX_META_STRING_LITERAL_SIZE 256 #define STR(literal) \ ::LiteralSizeLimiter< \ ::PushBackCharacter< \ ... \ ::PushBackCharacter< \ ::String<> \ , ::CharAt(literal, 0)>::type \ ... \ , ::CharAt(literal, 255)>::type \ , sizeof(literal) - 1>::type 

github

Il s'est avéré


 template <class S> std::string_view GetContent() { return std::string_view(Get<S>::value, sizeof(Get<S>::value)); } std::cout << GetContent<STR("Hello Habr!")>() << std::endl; 

L'implémentation que j'ai obtenue se trouve sur le github .

Il serait très intéressant pour moi d'entendre parler des applications possibles de ce mécanisme, différentes de celles que j'ai imaginées.

Source: https://habr.com/ru/post/fr438146/


All Articles