Auf dem Weg zum lang erwarteten Titel des 
Lead Senior C ++ Over-Engineer habe ich mich letztes Jahr entschlossen, das Spiel, das ich während der Arbeitszeit entwickle (Candy Crush Saga), unter Verwendung der Quintessenz des modernen C ++ (C ++ 17) neu zu schreiben. Und so wurde 
Meta Crush Saga geboren: ein 
Spiel, das in der Kompilierungsphase läuft . Ich war sehr inspiriert von Matt Birners 
Nibbler- Spiel, bei dem die Metapher 
mithilfe der reinen Metaprogrammierung auf Vorlagen nachgebildet wurde, um die berühmte Schlange mit dem Nokia 3310 nachzubilden.
"Was für ein 
Spiel läuft es in der Kompilierungsphase ?", "Wie sieht es aus?", "Welche Funktionalität von 
C ++ 17 haben Sie in diesem Projekt verwendet?", "Was haben Sie gelernt?" - Ähnliche Fragen können Ihnen in den Sinn kommen. Um sie zu beantworten, müssen Sie entweder den gesamten Beitrag lesen oder sich mit Ihrer inneren Faulheit abfinden und eine Videoversion des Beitrags ansehen - mein Bericht vom 
Meetup-Event in Stockholm:
Hinweis: Aus Gründen Ihrer psychischen Gesundheit und weil Sie sich 
menschlich irren , werden in diesem Artikel einige alternative Fakten aufgeführt.
Ein Spiel, das zur Kompilierungszeit läuft?
Ich denke, um zu verstehen, was ich unter dem "Konzept" eines 
Spiels verstehe, das 
in der Kompilierungsphase ausgeführt wird , müssen Sie den Lebenszyklus eines solchen Spiels mit dem Lebenszyklus eines normalen Spiels vergleichen.
Der Lebenszyklus eines regulären Spiels:
Als regelmäßiger Entwickler von Spielen mit normalem Leben, der an einem normalen Job mit normaler psychischer Gesundheit arbeitet, schreiben Sie normalerweise zunächst 
Spielelogik in Ihrer Lieblingssprache (natürlich in C ++!) Und führen dann den 
Compiler aus , um dies zu konvertieren, zu oft wie Spaghetti Logik in einer 
ausführbaren Datei . Nach einem Doppelklick auf die 
ausführbare Datei (oder ausgehend von der Konsole) erzeugt das Betriebssystem einen 
Prozess . Dieser 
Prozess führt die 
Spiellogik aus , die in 99,42% der Fälle aus einem Spielzyklus besteht. 
Der Spielzyklus aktualisiert den Status des Spiels gemäß bestimmten Regeln und 
Benutzereingaben und gibt 
den neu berechneten Status des Spiels immer wieder in Pixel wieder.
Der Lebenszyklus eines Spiels, das während des Kompilierungsprozesses ausgeführt wird:
Als Überentwickler, der sein neues cooles Kompilierungsspiel erstellt, verwenden Sie immer noch Ihre Lieblingssprache (natürlich immer noch C ++!), Um 
Spielelogik zu schreiben. Dann geht 
die Kompilierungsphase nach wie vor 
weiter , aber es gibt eine Wendung in der Handlung: Sie 
führen Ihre 
Spielelogik in der Kompilierungsphase aus. Sie können es "Ausführung" (Kompilierung) nennen. Und hier ist C ++ sehr nützlich; Es verfügt über Funktionen wie 
Template Meta Programming (TMP) und 
constexpr , mit denen Sie 
Berechnungen in der 
Kompilierungsphase durchführen können . Später werden wir die Funktionalität betrachten, die dafür verwendet werden kann. Da wir zu diesem Zeitpunkt die 
Logik des Spiels ausführen, müssen wir in diesem Moment auch 
die Eingabe des 
Spielers einfügen. Offensichtlich erstellt unser Compiler bei der Ausgabe weiterhin eine 
ausführbare Datei . Wofür kann es verwendet werden? Die ausführbare Datei enthält nicht mehr 
die Spielschleife , hat jedoch eine sehr einfache Aufgabe: Anzeigen eines neuen 
berechneten Status . Nennen wir diese 
ausführbare Datei den Renderer , und 
die gerenderten Daten werden gerendert . In unserem 
Rendering werden weder schöne Partikeleffekte noch Umgebungsokklusionsschatten enthalten sein, es wird ASCII sein. Das ASCII- 
Rendering des neuen berechneten 
Status ist eine praktische Eigenschaft, die dem Player leicht demonstriert werden kann. Zusätzlich kopieren wir sie in eine Textdatei. Warum eine Textdatei? Offensichtlich, weil es irgendwie mit dem 
Code kombiniert werden 
kann und alle vorherigen Schritte erneut ausführt, wodurch eine 
Schleife erhalten wird .
Wie Sie bereits verstehen können, besteht das 
während des Kompilierungsprozesses ausgeführte Spiel aus einem 
Spielzyklus, in dem jeder 
Frame des Spiels eine 
Kompilierungsphase darstellt . Jede 
Kompilierungsstufe berechnet einen neuen 
Status des Spiels, der dem Spieler angezeigt und in den nächsten 
Frame / die nächste 
Kompilierungsstufe eingefügt werden kann.
Sie können dieses großartige Diagramm so oft betrachten, wie Sie möchten, bis Sie verstehen, was ich gerade geschrieben habe:
Bevor wir uns mit den Details der Implementierung eines solchen Zyklus befassen, möchten Sie mir sicher die einzige Frage stellen ...
"Warum sich die Mühe machen, das zu tun?"
Denken Sie wirklich, dass es eine so grundlegende Frage ist, meine C ++ - Metaprogrammier-Idylle zu ruinieren? Ja, für nichts im Leben!
- Das erste und wichtigste ist, dass das in der Kompilierungsphase ausgeführte Spiel eine erstaunliche Ausführungszeit aufweist, da der Großteil der Berechnungen in der Kompilierungsphase ausgeführt wird . Die Laufzeitgeschwindigkeit ist der Schlüssel zum Erfolg unseres AAA-Spiels mit ASCII-Grafik!
- Sie verringern die Wahrscheinlichkeit, dass Krebstiere in Ihrem Repository erscheinen, und bitten Sie, das Spiel in Rust neu zu schreiben. Seine gut vorbereitete Rede wird auseinanderfallen, sobald Sie ihm erklären, dass zum Zeitpunkt der Kompilierung kein ungültiger Zeiger vorhanden sein kann. Die selbstbewussten Programmierer von Haskell können sogar die Typensicherheit in Ihrem Code bestätigen.
- Sie werden den Respekt des Javascript- Hipster-Königreichs gewinnen, in dem jedes neu gestaltete Framework mit einem starken NIH-Syndrom herrschen kann, vorausgesetzt, es hat einen coolen Namen.
- Ein Freund von mir pflegte zu sagen, dass jede Zeile Perl-Code de facto als sehr sicheres Passwort verwendet werden kann. Ich bin sicher, dass er nie versucht hat, Passwörter aus der C ++ - Kompilierungszeit zu generieren.
Und wie? Bist du mit meinen Antworten zufrieden? Dann sollte Ihre Frage vielleicht lauten: "Wie schaffen Sie das überhaupt?"
Eigentlich wollte ich unbedingt mit der in 
C ++ 17 hinzugefügten Funktionalität experimentieren. Nicht wenige Funktionen sollen die Effektivität der Sprache sowie die Metaprogrammierung (hauptsächlich constexpr) erhöhen. Ich dachte, anstatt kleine Codebeispiele zu schreiben, wäre es viel interessanter, all dies in ein Spiel zu verwandeln. Haustierprojekte sind eine großartige Möglichkeit, Konzepte zu lernen, die Sie in Ihrer Arbeit nicht oft verwenden müssen. Die Fähigkeit, die grundlegende Spielelogik zur Kompilierungszeit erneut auszuführen, beweist, dass Vorlagen und constepxr 
Turing-vollständige Teilmengen der C ++ - Sprache sind.
Meta Crush Saga Game Review
Match-3-Spiel:
Meta Crush Saga ist ein 
Spiel , das 
Bejeweled und 
Candy Crush Saga ähnelt. Der Kern der Spielregeln besteht darin, drei Kacheln mit demselben Muster zu verbinden, um Punkte zu erhalten. Hier ist ein kurzer Blick auf den 
Stand des Spiels , den ich "gedumpt" habe (Dumping in ASCII ist verdammt einfach zu bekommen):
  R "(
     Meta Crush Saga      
 ------------------------  
 |  | 
 |  RBGBBYGR | 
 |  | 
 |  | 
 |  YYGRBGBR | 
 |  | 
 |  | 
 |  RBYRGRYG | 
 |  | 
 |  | 
 |  RYBY (R) YGY | 
 |  | 
 |  | 
 |  BGYRYGGR | 
 |  | 
 |  | 
 |  RYBGYBBG | 
 |  | 
 ------------------------  
 > Punktzahl: 9009
 > bewegt sich: 27
 ) " Das Gameplay dieses Match-3-Spiels selbst ist nicht besonders interessant, aber was ist mit der Architektur, auf der alles funktioniert? Damit Sie es verstehen, werde ich versuchen, jeden Teil des Lebenszyklus dieses Spiels zur 
Kompilierungszeit in Form von Code zu erklären.
Spielstatus-Injektion:
Wenn Sie ein leidenschaftlicher C ++ - Liebhaber oder Pedant sind, haben Sie möglicherweise bemerkt, dass der vorherige Spielstatus-Dump mit dem folgenden Muster beginnt: 
R "( . Tatsächlich handelt es sich um ein 
rohes C ++ 11-Zeichenfolgenliteral , was bedeutet, dass ich Sonderzeichen, z. B. 
Übersetzung, nicht entkommen muss 
Zeichenfolgen : Das rohe Zeichenfolgenliteral wird in einer Datei namens 
current_state.txt gespeichert.
Wie fügen wir diesen aktuellen Status des Spiels in einen Kompilierungsstatus ein? Fügen wir es einfach zu den Schleifeneingängen hinzu!
Unabhängig davon , ob es sich um eine 
TXT- Datei oder eine 
H- Datei handelt, 
funktioniert die 
Include- Anweisung des C-Präprozessors auf dieselbe Weise: Sie kopiert den Inhalt der Datei an ihren Speicherort. Hier kopiere ich das Raw-String-Literal des Spielstatus in ASCII in eine Variable namens 
game_state_string .
Beachten Sie, dass die Header- 
Datei loop_inputs.hpp auch die Tastatureingabe auf den aktuellen 
Frame- / Kompilierungsschritt erweitert. Im Gegensatz zum Status des Spiels ist der Status der Tastatur recht klein und kann leicht als Definition eines Präprozessors abgerufen werden.
Berechnen eines neuen Status zur Kompilierungszeit:
Nachdem wir genügend Daten gesammelt haben, können wir den neuen Status berechnen. Endlich haben wir den Punkt erreicht, an dem wir die Datei 
main.cpp schreiben müssen:
 
Seltsam, aber dieser C ++ - Code sieht nicht so verwirrend aus, wenn man bedenkt, was er tut. Der größte Teil des Codes wird in der Kompilierungsphase ausgeführt, folgt jedoch den traditionellen OOP- und prozeduralen Programmierparadigmen. Nur die letzte Zeile - das Rendern - ist ein Hindernis, um Berechnungen zur Kompilierungszeit vollständig ausführen zu können. Wie wir weiter unten sehen werden, können wir in C ++ 17 eine recht elegante Metaprogrammierung erzielen, wenn wir ein wenig constexpr an die richtigen Stellen werfen. Ich finde die Freiheit, die C ++ uns bei der gemischten Ausführung zur Laufzeit und Kompilierung gibt, sehr erfreulich.
Sie werden auch feststellen, dass dieser Code nur einen Frame ausführt, es gibt keine 
Spielschleife . Lösen wir dieses Problem!
Wir kleben alles zusammen:
Wenn Sie meine Tricks mit 
C ++ abschrecken , dann hoffe ich, dass es Ihnen nichts ausmacht, meine 
Bash- Fähigkeiten zu sehen. Tatsächlich ist meine 
Spieleschleife nichts anderes als ein 
Bash-Skript , das ständig kompiliert wird.
 
Tatsächlich hatte ich einige Probleme, Tastatureingaben von der Konsole zu erhalten. Anfangs wollte ich parallel zur Kompilierung gehen. Nach vielen Versuchen und Irrtümern gelang es mir, etwas mehr oder weniger mit dem 
read von 
Bash zum Laufen zu bringen . Ich wage es nie, den Zauberer 
Bash im Zweikampf zu bekämpfen - diese Sprache ist zu unheimlich!
Ich muss also zugeben, dass ich zur Verwaltung des Spielzyklus auf eine andere Sprache zurückgreifen musste. Obwohl mich technisch nichts daran hinderte, diesen Teil des Codes in C ++ zu schreiben. Darüber hinaus negiert dies nicht die Tatsache, dass 90% der Logik meines Spiels im 
g ++ - Kompilierungsteam ausgeführt werden, was ziemlich erstaunlich ist!
Ein kleines Gameplay, um Ihren Augen eine Pause zu gönnen:
Nachdem Sie die Qual erlebt haben, die Architektur des Spiels zu erklären, ist es an der Zeit, auffällige Bilder zu malen:
Dieses pixelige GIF zeigt, wie ich 
Meta Crush Saga spiele. Wie Sie sehen können, läuft das Spiel reibungslos genug, um in Echtzeit spielbar zu sein. Offensichtlich ist sie nicht so attraktiv, dass ich ihr Twitch streamen und die neue Pewdiepie werden kann, aber sie arbeitet!
Einer der unterhaltsamen Aspekte beim Speichern des 
Status eines Spiels in einer 
TXT- Datei ist die Möglichkeit, Extremfälle zu betrügen oder extrem praktisch zu testen.
Nachdem ich Sie kurz in die Architektur eingeführt habe, werden wir uns mit der in diesem Projekt verwendeten C ++ 17-Funktionalität befassen. Ich werde die Spielelogik nicht im Detail betrachten, da sie sich ausschließlich auf Match-3 bezieht, sondern über Aspekte von C ++ sprechen, die in anderen Projekten angewendet werden können.
Meine Tutorials zu C ++ 17:
Im Gegensatz zu C ++ 14, das hauptsächlich kleinere Korrekturen enthielt, kann uns der neue C ++ 17-Standard viel bieten. Es gab Hoffnungen, dass endlich die lang erwarteten Funktionen (Module, Coroutinen, Konzepte ...) endlich auftauchen würden, aber ... im Allgemeinen ... erschienen sie nicht; es hat viele von uns verärgert. Nachdem wir die Trauer beseitigt hatten, fanden wir viele kleine unerwartete Schätze, die dennoch in den Standard fielen.
Ich wage zu sagen, dass Kinder, die Metaprogrammierung lieben, dieses Jahr zu verwöhnt sind! Durch separate geringfügige Änderungen und Ergänzungen der Sprache können Sie jetzt Code schreiben, der zur Kompilierungszeit und danach zur Laufzeit sehr gut funktioniert.
Constepxr in allen Bereichen:
Wie Ben Dean und Jason Turner in ihrem 
Bericht zu C ++ 14 vorausgesagt haben, können Sie mit C ++ die Kompilierung von Werten zur Kompilierungszeit mit dem allmächtigen Schlüsselwort 
constexpr schnell verbessern. Wenn Sie dieses Schlüsselwort an den richtigen Stellen suchen, können Sie dem Compiler mitteilen, dass der Ausdruck konstant ist und direkt zur Kompilierungszeit ausgewertet werden kann. In 
C ++ 11 konnten wir diesen Code bereits schreiben:
 constexpr int factorial(int n)  
Obwohl das Schlüsselwort 
constexpr sehr leistungsfähig ist, gibt es einige Verwendungsbeschränkungen, die es schwierig machen, ausdrucksstarken Code auf diese Weise zu schreiben.
C ++ 14 hat die Anforderungen an 
constexpr erheblich reduziert und ist viel natürlicher zu verwenden. Unsere vorherige Fakultätsfunktion kann wie folgt umgeschrieben werden:
 constexpr int factorial(int n) { if (n <= 1) { return 1; } return n * factorial(n - 1); } 
C ++ 14 hat die Regel 
beseitigt, dass eine 
constexpr-Funktion nur aus einer return-Anweisung bestehen sollte, was uns gezwungen hat, den 
ternären Operator als Hauptbaustein zu verwenden. Jetzt bringt 
C ++ 17 noch mehr 
constexpr- Keyword-Anwendungen, die wir untersuchen können!
Verzweigung zur Kompilierungszeit:
Warst du jemals in einer Situation, in der du abhängig von dem Vorlagenparameter, den du manipulierst, ein anderes Verhalten haben musst? Angenommen, wir benötigen eine parametrisierte Funktion 
serialize , die 
.serialize() wenn das Objekt dies bereitstellt, andernfalls wird dazu 
to_string . Wie in diesem 
Beitrag über SFINAE ausführlicher 
erläutert , müssen Sie höchstwahrscheinlich einen solchen Alien-Code schreiben:
 template <class T> std::enable_if_t<has_serialize_v<T>, std::string> serialize(const T& obj) { return obj.serialize(); } template <class T> std::enable_if_t<!has_serialize_v<T>, std::string> serialize(const T& obj) { return std::to_string(obj); } 
Nur in einem Traum könnten Sie diesen hässlichen 
Trick von SFINAE-Trick zu 
C ++ 14 in solch großartigen Code umschreiben:
 
Als Sie aufwachten und anfingen, echten 
C ++ 14-Code zu schreiben, gab Ihr Compiler leider eine unangenehme Nachricht über den Aufruf von 
serialize(42); . Es wurde erklärt, dass ein 
obj Typ 
int keine Mitgliedsfunktion 
serialize() . Egal wie wütend es Sie macht, der Compiler hat Recht! Mit diesem Code wird er immer versuchen, beide Zweige zu kompilieren - 
return obj.serialize(); und
return std::to_string(obj); . Für 
int branch 
return obj.serialize(); Es kann sich durchaus als eine Art toter Code 
has_serialize(obj) , da 
has_serialize(obj) immer 
false has_serialize(obj) , der Compiler ihn jedoch weiterhin kompilieren muss.
Wie Sie wahrscheinlich vermutet haben, 
bewahrt uns 
C ++ 17 vor einer solch unangenehmen Situation, da es möglich war, 
constexpr nach der if-Anweisung hinzuzufügen, 
um die Verzweigung zur Kompilierungszeit zu erzwingen und nicht verwendete Konstruktionen zu verwerfen:
 
Dies ist offensichtlich eine enorme Verbesserung 
gegenüber dem SFINAE-Trick, den wir zuvor anwenden mussten. Danach bekamen wir die gleiche Sucht wie Ben und Jason - wir begannen überall und immer mit 
constexpr . 
Leider gibt es einen anderen Ort, an den 
das Schlüsselwort 
constexpr passen würde, aber noch nicht verwendet wird: 
Parameter constexpr .
Constexpr-Parameter:
Wenn Sie vorsichtig sind, stellen Sie möglicherweise ein seltsames Muster im vorherigen Codebeispiel fest. Ich spreche von Schleifeneingängen:
 
Warum ist die Variable 
game_state_string in einem constexpr-Lambda eingekapselt? Warum macht sie sie nicht zu einer 
globalen Variablen constexpr ?
Ich wollte diese Variable und ihren Inhalt tief in einige Funktionen übergeben. Zum Beispiel müssen 
Sie es an mein 
parse_board übergeben und in einigen konstanten Ausdrücken verwenden:
 constexpr int parse_board_size(const char* game_state_string); constexpr auto parse_board(const char* game_state_string) { std::array<GemType, parse_board_size(game_state_string)> board{};  
Wenn wir diesen Weg gehen, beschwert sich der mürrische Compiler, dass der Parameter 
game_state_string kein konstanter Ausdruck ist. Wenn ich mein Kachelarray erstelle, muss ich seine feste Kapazität direkt berechnen (wir können zur Kompilierungszeit keine Vektoren verwenden, da sie eine Speicherzuweisung erfordern) und es als Argument an die Wertvorlage in 
std :: array übergeben . Daher muss der Ausdruck 
parse_board_size (game_state_string) ein konstanter Ausdruck sein. Obwohl 
parse_board_size explizit als 
constexpr markiert ist, ist und kann 
game_state_string nicht sein! In diesem Fall stören uns zwei Regeln:
- Argumente einer constexpr-Funktion sind nicht constexpr!
- Und wir können constexpr nicht vor ihnen hinzufügen!
All dies 
läuft darauf hinaus, dass 
constexpr-Funktionen sowohl für die Berechnung der Laufzeit als auch der Kompilierungszeit anwendbar sein 
MÜSSEN . Unter der Annahme, dass 
constexpr-Parameter vorhanden sind , können diese zur Laufzeit nicht verwendet werden.
Glücklicherweise gibt es eine Möglichkeit, dieses Problem zu lösen. Anstatt den Wert als regulären Parameter einer Funktion zu akzeptieren, können wir diesen Wert in einen Typ einkapseln und diesen Typ als Vorlagenparameter übergeben:
 template <class GameStringType> constexpr auto parse_board(GameStringType&&) { std::array<CellType, parse_board_size(GameStringType::value())> board{};  
In diesem Codebeispiel erstelle ich einen 
GameString- Strukturtyp mit einer statischen Elementfunktion constexpr 
value () , die das Zeichenfolgenliteral zurückgibt, das ich an 
parse_board übergeben 
möchte . In 
parse_board erhalte ich diesen Typ über den Vorlagenparameter 
GameStringType , wobei die Regeln zum Extrahieren von Vorlagenargumenten verwendet werden. Mit einem 
GameStringType kann 
ich aufgrund der Tatsache, dass 
value () constexpr ist, einfach zum richtigen Zeitpunkt den statischen Member-Funktionswert 
() aufrufen, um ein String-Literal auch an Stellen abzurufen, an denen konstante Ausdrücke benötigt werden.
Wir haben es geschafft, das Literal zu kapseln, um es mit 
constexpr irgendwie an 
parse_board zu übergeben. Es ist jedoch sehr ärgerlich, jedes Mal einen neuen Typ definieren zu müssen, wenn Sie ein neues 
parse_board- Literal senden 
müssen : "... 
etwas1 ...", "... 
etwas2 ...". Um dieses Problem in 
C ++ 11 zu lösen, können Sie eine hässliche Makro- und indirekte Adressierung mit anonymer Union und Lambda anwenden. Michael Park hat dieses Thema in 
einem seiner Beiträge gut erklärt .
In 
C ++ 17 ist die Situation noch besser. Wenn wir die Anforderungen für die Übergabe unseres String-Literal auflisten, erhalten wir Folgendes:
- Funktion generiert
- Das ist constexpr
- Mit einem eindeutigen oder anonymen Namen
Diese Anforderungen sollten Ihnen einen Hinweis geben. Was wir brauchen, ist 
constexpr Lambda ! Und in 
C ++ 17 haben sie ganz natürlich die Möglichkeit hinzugefügt, das 
Schlüsselwort constexpr für Lambda-Funktionen zu verwenden. Wir können unseren Beispielcode wie folgt umschreiben:
 template <class LambdaType> constexpr auto parse_board(LambdaType&& get_game_state_string) { std::array<CellType, parse_board_size(get_game_state_string())> board{};  
Glauben Sie mir, dies sieht bereits viel praktischer aus als das vorherige Hacken in 
C ++ 11 mit Makros. Ich habe diesen großartigen Trick dank 
Björn Fahler entdeckt , einem Mitglied der C ++ Mitap-Gruppe, an der ich teilnehme. Lesen Sie mehr über diesen Trick in seinem 
Blog . Es ist auch zu berücksichtigen, dass das Schlüsselwort 
constexpr in diesem Fall tatsächlich optional ist: Alle 
Lambdas mit der Fähigkeit, 
constexpr zu werden, sind standardmäßig diese. Das explizite Hinzufügen von 
constexpr ist eine Signatur, die unsere Fehlerbehebung vereinfacht.
Jetzt müssen Sie verstehen, warum ich gezwungen war, ein 
constexpr Lambda zu verwenden, um eine Zeichenfolge weiterzugeben, die den Status des Spiels darstellt. Schauen Sie sich diese Lambda-Funktion an und Sie werden wieder eine andere Frage haben. Was ist dieser 
constexpr_string- Typ, den ich auch zum 
Umschließen des Aktienliteral verwende?
constexpr_string und constexpr_string_view:
Wenn Sie mit Zeichenfolgen arbeiten, sollten Sie sie nicht im C-Stil verarbeiten. Sie müssen all diese nervigen Algorithmen vergessen, die rohe Iterationen ausführen und auf Null-Vervollständigung prüfen! Die von 
C ++ angebotene Alternative sind die allmächtigen 
Algorithmen std :: string und 
STL . Leider erfordert 
std :: string möglicherweise eine Speicherzuweisung auf dem Heap (auch bei Small String Optimization), um dessen Inhalt zu speichern. Ein oder zwei Standards zurück, wir könnten 
constexpr new / delete verwenden oder 
constexpr-Allokatoren an 
std :: string übergeben , aber jetzt müssen wir eine andere Lösung finden.
Mein Ansatz war es, eine 
constexpr_string- Klasse mit einer festen Kapazität zu schreiben. Diese Kapazität wird als Parameter an die Wertvorlage übergeben. Hier ist ein kurzer Überblick über meine Klasse:
 template <std::size_t N>  
Meine 
constexpr_string- Klasse versucht, die 
std :: string- Schnittstelle so nah wie möglich zu imitieren (für die Operationen, die ich benötige): Wir können 
Iteratoren des Anfangs und des Endes anfordern, die 
Größe (Größe) abrufen , auf die 
Daten (Daten) zugreifen, einen Teil davon 
löschen (löschen) , abrufen Teilstring mit 
substr und so weiter. Dies 
macht es sehr einfach, einen Code von 
std :: string in 
constexpr_string zu 
konvertieren . Sie fragen sich vielleicht, was passiert, wenn wir Operationen verwenden müssen, die normalerweise in 
std :: string hervorgehoben werden müssen . In solchen Fällen musste ich sie in 
unveränderliche Operationen konvertieren, die eine neue Instanz von 
constexpr_string erstellen .
Werfen wir einen Blick auf die 
Append- Operation:
 template <std::size_t N>  
Sie benötigen keinen Fields-Preis, um davon auszugehen, dass bei einer Zeichenfolge der Größe 
N und einer Zeichenfolge der Größe 
M eine Zeichenfolge der Größe 
N + M ausreicht, um deren Verkettung zu speichern. Wir verschwenden möglicherweise einen Teil des "Repository zur Kompilierungszeit", da beide Leitungen möglicherweise nicht die gesamte Kapazität nutzen, dies ist jedoch aus Bequemlichkeitsgründen ein eher geringer Preis. 
Natürlich habe ich auch ein Duplikat von 
std :: string_view geschrieben , das 
constexpr_string_view heißt.
Mit diesen beiden Klassen war ich bereit, eleganten Code zu schreiben, um meinen 
Spielstatus zu analysieren. Denken Sie an so etwas:
 constexpr auto game_state = constexpr_string(“...something...”);  
Es war ziemlich einfach, die Juwelen auf dem Spielfeld zu durchlaufen. Haben Sie in diesem Codebeispiel übrigens eine weitere wertvolle Funktion von 
C ++ 17 bemerkt?
Ja! Ich musste die Kapazität von 
constexpr_string beim 
Erstellen nicht explizit angeben. Bisher mussten wir bei der Verwendung einer 
Klassenvorlage deren Argumente explizit angeben. Um diese 
Probleme zu vermeiden, erstellen wir 
make_xxx- Funktionen, da die Parameter 
von Funktionsvorlagen nachverfolgt werden können. Sehen Sie, wie das 
Verfolgen von Argumenten für 
Klassenvorlagen unser Leben zum Besseren verändert:
 template <int N> struct constexpr_string { constexpr_string(const char(&a)[N]) {}  
In einigen schwierigen Situationen müssen Sie dem Compiler helfen, die Argumente korrekt zu berechnen. Wenn Sie auf ein solches Problem stoßen, lesen Sie die 
Handbücher für benutzerdefinierte Argumentberechnungen .
Kostenloses Essen von STL:
Nun, wir können immer alles selbst umschreiben. Aber vielleicht haben Komiteemitglieder in der Standardbibliothek großzügig etwas für uns vorbereitet?
Neue Hilfstypen:
In 
C ++ 17 werden 
std :: variante und 
std :: optional zu den Standardwörterbuchtypen hinzugefügt, basierend auf 
constexpr . Das erste ist sehr interessant, weil es uns erlaubt, typsichere Assoziationen auszudrücken, aber die Implementierung in der 
libstdc ++ - Bibliothek mit 
GCC 7.2 hat Probleme bei der Verwendung konstanter Ausdrücke. Aus diesem Grund habe ich die Idee aufgegeben, meinem Code eine 
std :: -Variante hinzuzufügen und nur 
std :: optional zu verwenden .
Mit Typ T können wir mit dem Typ std :: optional einen neuen Typ std :: optional <T> erstellen , der entweder einen Wert vom Typ T oder nichts enthalten kann. Dies ist ziemlich ähnlich zu aussagekräftigen Typen, die einen undefinierten Wert in C # zulassen . Schauen wir uns die Funktion find_in_board an , die die Position des ersten Elements in einem Feld zurückgibt, das bestätigt, dass das Prädikat korrekt ist. Möglicherweise befindet sich kein solches Element auf dem Feld. Um diese Situation zu bewältigen, muss der Positionstyp optional sein: template <class Predicate> constexpr std::optional<std::pair<int, int>> find_in_board(GameBoard&& g, Predicate&& p) { for (auto item : g.items()) { if (p(item)) { return {item.x, item.y}; }  
Zuvor mussten wir entweder auf die Semantik von Zeigern zurückgreifen oder einen „leeren Zustand“ direkt zum Positionstyp hinzufügen oder einen Booleschen Wert zurückgeben und den Ausgabeparameter übernehmen . Zugegeben, das war ziemlich umständlich!Einige bereits vorhandene Typen erhielten auch constexpr- Unterstützung : Tupel und Paar . Ich werde ihre Verwendung nicht im Detail erklären, da bereits viel über sie geschrieben wurde, aber ich werde eine meiner Enttäuschungen teilen. Das Komitee fügte dem Standard syntaktischen Zucker hinzu , um die in einem Tupel oder Paar enthaltenen Werte zu extrahieren . Diese neue Art der Deklaration wird als strukturierte Bindung bezeichnet, verwendet Klammern, um anzugeben, in welchen Variablen das geteilte Tupel oder Paar gespeichert werden soll : std::pair<int, int> foo() { return {42, 1337}; } auto [x, y] = foo();  
Sehr schlau! Aber es ist schade, dass die Komiteemitglieder [konnten, wollten nicht, fanden nicht die Zeit, vergaßen] sie freundlich zu constexpr machen . Ich würde so etwas erwarten: constexpr auto [x, y] = foo();  
Jetzt haben wir komplexe Container und Hilfstypen, aber wie können wir sie bequem manipulieren?Algorithmen:
Das Aktualisieren eines Containers für die Verarbeitung von constexpr ist eine ziemlich eintönige Aufgabe. Im Vergleich dazu scheint es einfach genug zu sein, constexpr auf nicht modifizierende Algorithmen zu portieren . Es ist jedoch ziemlich seltsam, dass wir in C ++ 17 keine Fortschritte in diesem Bereich gesehen haben, sondern nur in C ++ 20 . Zum Beispiel haben die wunderbaren std :: find- Algorithmen keine constexpr- Signaturen erhalten .Aber keine Angst! Wie Ben und Jason erklärt haben, können Sie den Algorithmus leicht in constexpr umwandeln, indem Sie einfach die aktuelle Implementierung kopieren (aber vergessen Sie nicht die Urheberrechte). cppreference ist gut. Meine Damen und Herren, ich präsentiere Ihnen Ihre Aufmerksamkeitconstexpr std :: find : template<class InputIt, class T> constexpr InputIt find(InputIt first, InputIt last, const T& value) // ^ !!!    constexpr. { for (; first != last; ++first) { if (*first == value) { return first; } } return last; }  
Ich kann schon von den Ständen die Schreie der Optimierungsfans hören! Ja, nur das Hinzufügen von constexpr vor dem von cppreference freundlicherweise bereitgestellten Beispielcode führt zur Laufzeit möglicherweise nicht zu einer idealen Geschwindigkeit . Wenn wir diesen Algorithmus jedoch verbessern müssen, wird er für die Geschwindigkeit bei der Kompilierung benötigt . Soweit ich weiß, sind einfache Lösungen für die Kompilierungsgeschwindigkeit am besten.Geschwindigkeit und Fehler:
Entwickler eines AAA-Spiels sollten in die Lösung dieser Probleme investieren, oder?Geschwindigkeit:
Als ich es schaffte, eine halb funktionierende Version von Meta Crush Saga zu erstellen , verlief die Arbeit reibungsloser. Tatsächlich habe ich auf meinem alten Laptop mit i5, das auf 1,80 GHz übertaktet wurde, etwas mehr als 3 FPS (Bilder pro Sekunde) erreicht (Frequenz ist in diesem Fall wichtig). Wie in jedem Projekt wurde mir schnell klar, dass der zuvor geschriebene Code ekelhaft war, und ich begann, das Parsen des Spielzustands mithilfe von constexpr_string und Standardalgorithmen neu zu schreiben . Obwohl dies die Wartung des Codes wesentlich komfortabler machte, wirkten sich die Änderungen ernsthaft auf die Geschwindigkeit aus. Die neue Decke beträgt 0,5 FPS .Trotz des alten Sprichworts über C ++ sind „Null-Kopf-Abstraktionen“ nicht für Berechnungen zur Kompilierungszeit anwendbar. Dies ist ziemlich logisch, wenn wir den Compiler als Interpreter eines „Kompilierungszeitcodes“ betrachten. Verbesserungen für verschiedene Compiler sind weiterhin möglich, aber es gibt auch Wachstumschancen für uns, die Autoren eines solchen Codes. Hier ist eine unvollständige Liste von Beobachtungen und Tipps, die ich gefunden habe, möglicherweise spezifisch für GCC:- C-Arrays funktionieren viel besser als std :: array . std :: array ist ein bisschen moderne C ++ - Kosmetik auf einem Array im C-Stil, und Sie müssen einen Preis dafür zahlen, wenn Sie es unter solchen Bedingungen verwenden.
- , ( ) . , , , . : , , , , ( ) , .
- , . , .
- . GCC. , «».
:
Oft hat mein Compiler schreckliche Kompilierungsfehler ausgegeben, und meine Codelogik hat darunter gelitten. Aber wie findet man den Ort, an dem sich der Käfer versteckt? Ohne Debugger und printf werden die Dinge komplizierter. Wenn Ihr metaphorischer „Bart des Programmierers“ noch nicht auf die Knie gegangen ist (sowohl der metaphorische als auch der echte Bart von mir sind noch weit von diesen Erwartungen entfernt), haben Sie möglicherweise keine Motivation, Templight zu verwenden oder den Compiler zu debuggen.Unser erster Freund wird static_assert sein , was uns die Möglichkeit gibt, den booleschen Wert der Kompilierungszeit zu überprüfen. Unser zweiter Freund wird ein Makro sein, das constexpr aktiviert und deaktiviert, wo immer dies möglich ist: #define CONSTEXPR constexpr  
Mit diesem Makro können wir die Logik zur Laufzeit arbeiten lassen, was bedeutet, dass wir einen Debugger daran anhängen können.Meta Crush Saga II - strebe das Gameplay zur Laufzeit vollständig an:
Offensichtlich wird Meta Crush Saga dieses Jahr nicht die The Game Awards gewinnen . Es hat großes Potenzial, aber das Gameplay wird zum Zeitpunkt der Kompilierung nicht vollständig ausgeführt . Dies kann Hardcore-Gamer ärgern ... Ich kann das Bash-Skript nur loswerden, wenn jemand in der Kompilierungsphase Tastatureingaben und unreine Logik hinzufügt (und das ist offener Wahnsinn!). Ich glaube jedoch, dass ich eines Tages die ausführbare Datei des Renderers vollständig verlassen und den Status des Spiels zur Kompilierungszeit anzeigen kann :Der Verrückte mit dem Alias Saarraz hat GCC erweitert , um der Sprache das Konstrukt static_print hinzuzufügen . Dieses Konstrukt sollte mehrere konstante Ausdrücke oder Zeichenfolgenliterale verwenden und diese in der Kompilierungsphase ausgeben. Ich würde mich freuen, wenn ein solches Tool zum Standard hinzugefügt oder zumindest static_assert erweitert würde, damit es konstante Ausdrücke akzeptiert .In C ++ 17 kann es jedoch eine Möglichkeit geben, dieses Ergebnis zu erzielen. Compiler geben bereits zwei Dinge aus - Fehler und Warnungen ! Wenn wir die Warnungen irgendwie verwalten oder an unsere Bedürfnisse anpassen können, erhalten wir bereits eine würdige Schlussfolgerung. Ich habe insbesondere verschiedene Lösungen ausprobiertveraltetes Attribut : template <char... words> struct useless { [[deprecated]] void call() {}  
Obwohl die Ausgabe offensichtlich vorhanden ist und analysiert werden kann, ist der Code leider nicht spielbar! Wenn Sie zufällig Mitglied einer Geheimgesellschaft von C ++ - Programmierern sind, die während der Kompilierung Ausgaben ausführen können, werde ich Sie gerne in meinem Team einstellen, um die perfekte Meta Crush Saga II zu erstellen !Schlussfolgerungen:
Am Ende habe ich dir mein Betrugsspiel verkauft . Ich hoffe, Sie finden diesen Beitrag neugierig und lernen beim Lesen etwas Neues. Wenn Sie Fehler oder Möglichkeiten zur Verbesserung des Artikels finden, kontaktieren Sie mich.Ich möchte dem SwedenCpp-Team dafür danken, dass ich meinen Projektbericht bei einer ihrer Veranstaltungen durchführen durfte. Darüber hinaus möchte ich Alexander Gurdeev meinen tiefen Dank aussprechen , der mir geholfen hat, die wesentlichen Aspekte der Meta Crush Saga zu verbessern .