Wir setzen die Zeilen in die Vorlagenparameter

Modernes C ++ hat uns eine Reihe von Funktionen gebracht, die bisher in der Sprache schmerzlich fehlten. Um einen ähnlichen Effekt zu erzielen, wurden lange Zeit atemberaubende Krücken erfunden, die hauptsächlich aus sehr großen Fußtüchern mit Mustern und Makros (oft auch autogen) bestehen. Aber jetzt entsteht von Zeit zu Zeit ein Bedarf an Möglichkeiten, die noch nicht in der Sprache sind. Und wir beginnen wieder, komplexe Designs aus Vorlagen und Makros neu zu erfinden, sie zu generieren und das Verhalten zu erreichen, das wir brauchen. Das ist so eine Geschichte.

Im letzten halben Jahr brauchte ich zweimal Werte, die in den Vorlagenparametern verwendet werden konnten. Gleichzeitig wollte ich für diese Werte lesbare Namen haben und die Notwendigkeit ausschließen, diese Namen im Voraus zu deklarieren. Die spezifischen Aufgaben, die ich gelöst habe, waren ein separates Problem. Vielleicht schreibe ich später separate Posts darüber, irgendwo im Hub für abnormale Programmierung. Jetzt werde ich über den Ansatz sprechen, mit dem ich dieses Problem gelöst habe.

Wenn es also um Vorlagenparameter geht, können wir entweder den Typ oder den statischen const-Wert verwenden. Für die meisten Aufgaben ist dies mehr als genug. Wir möchten in den Parametern lesbare Bezeichner verwenden - wir deklarieren die Struktur, Aufzählung oder Konstante und verwenden sie. Probleme beginnen, wenn wir diesen Bezeichner nicht im Voraus bestimmen können und dies an Ort und Stelle tun möchten.

Es wäre möglich, eine Struktur oder Klasse direkt im Template-Parameter zu deklarieren. Dies funktioniert sogar, wenn die Vorlage mit diesem Parameter nichts tut, was eine vollständige Beschreibung der Struktur erfordert. Außerdem können wir den Namespace, in dem eine solche Struktur deklariert ist, nicht steuern. Und völlig identisch aussehende Vorlagenersetzungen werden zu völlig anderem Code, wenn sich diese Zeilen in benachbarten Klassen oder Namespaces befinden.

Sie müssen Literale verwenden, und von allen Literalen in C ++ können nur ein Zeichenliteral und ein Zeichenfolgenliteral als lesbar bezeichnet werden. Ein Zeichenliteral ist jedoch auf vier Zeichen beschränkt (bei Verwendung von char32_t), und ein Zeichenfolgenliteral ist ein Array von Zeichen, dessen Wert nicht an die Vorlagenparameter übergeben werden kann.

Es stellt sich eine Art Teufelskreis heraus. Sie müssen entweder etwas im Voraus deklarieren oder unbequeme Bezeichner verwenden. Versuchen wir, die Sprache zu finden, an die sie nicht angepasst ist. Was ist, wenn Sie ein Makro implementieren, das für die Verwendung in Vorlagenargumenten aus einem Zeichenfolgenliteral geeignet ist?

Lassen Sie uns die Struktur für die Zeichenfolge erstellen


Lassen Sie uns zunächst die Basis für die Zeichenfolge erstellen. In C ++ 11 wurden verschiedene Vorlagenargumente angezeigt.
Wir deklarieren eine Struktur, die Zeichenfolgen in den Argumenten enthält:

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

Github

Es funktioniert. Wir können diese Zeilen sogar sofort so verwenden:

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

Ziehen Sie nun diese Zeile in die Laufzeit


Großartig. Es wäre nicht schlecht, den Wert dieser Zeichenfolge zur Laufzeit abrufen zu können. Es gebe eine zusätzliche Vorlagenstruktur, die Argumente aus einer solchen Zeichenfolge extrahiert und daraus eine Konstante macht:

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

Das funktioniert auch. Da unsere Zeilen am Ende kein '\ 0' enthalten, müssen Sie sorgfältig mit dieser Konstante arbeiten (meiner Meinung nach ist es besser, sofort eine string_view mit der Konstanten und der Größe davon in den Konstruktorargumenten zu erstellen). Man könnte einfach '\ 0' am Ende des Arrays hinzufügen, aber das ist für meine Aufgaben nicht notwendig.

Überprüfen Sie, ob wir solche Zeichenfolgen manipulieren können


Okay, was kann man sonst noch mit solchen Saiten machen? Zum Beispiel verketten:

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

Github

Im Prinzip können Sie mehr oder weniger jede Operation ausführen (ich habe es nicht ausprobiert, weil ich es nicht brauche, aber ich kann mir nur vorstellen, wie Sie nach einem Teilstring suchen oder sogar einen Teilstring ersetzen können).
Jetzt haben wir die Hauptfrage, wie Zeichen in der Kompilierungszeit aus einem Zeichenfolgenliteral extrahiert und in die Vorlagenargumente eingefügt werden können.

Zeichne die Eule. Schreibe ein Makro.


Beginnen wir mit einer Möglichkeit, Zeichen einzeln in die Vorlagenargumente einzufügen:

 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

Ich verwende eine separate Spezialisierung für das Zeichen '\ 0', um es nicht der verwendeten Zeichenfolge hinzuzufügen. Außerdem vereinfacht dies die anderen Teile des Makros etwas.

Die gute Nachricht ist, dass ein String-Literal ein Parameter für die Funktion constexpr sein kann. Wir werden eine Funktion schreiben, die ein Zeichen nach Index in einer Zeichenfolge oder '\ 0' zurückgibt, wenn die Zeichenfolge kürzer als der Index ist (hier ist die PushBackCharacter-Spezialisierung für das Zeichen '\ 0' nützlich).

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

Grundsätzlich können wir schon so etwas schreiben:

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

Wir haben ein solches Fußtuch in unser Makro gelegt, aber echter (wir können Skripte schreiben, um Code zu generieren), und das war's!

Es gibt eine Nuance. Wenn die Anzahl der Zeichen in der Zeile größer ist als die Verschachtelungsebenen im Makro, wird die Zeile einfach abgeschnitten und wir werden es nicht einmal bemerken. Das Durcheinander.

Lassen Sie uns eine weitere Struktur erstellen, die den ankommenden String in keiner Weise konvertiert, sondern static_assert ausführt, damit seine Länge eine Konstante nicht überschreitet:

 #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 

Nun, das Makro sieht ungefähr so ​​aus:

 #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

Es stellte sich heraus


 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; 

Die Implementierung, die ich bekommen habe, ist auf dem Github zu finden .

Es wäre sehr interessant für mich, etwas über die möglichen Anwendungen dieses Mechanismus zu erfahren, die sich von denen unterscheiden, die ich mir ausgedacht habe.

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


All Articles