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 .