Bei Epic Games haben wir ein paar einfache Programmierungsstandards und Konventionen. Dieses Dokument spiegelt den Zustand der aktuellen Programmierungsstandards von Epic Games wider. Das Folgen Programmierungsstandards ist obligatorisch.
Programmierungskonventionen sind für Programmierer aus mehreren Gründen wichtig:
-
80 % der Lebenszeitkosten eines Teils der Software gehen auf die Wartung.
-
Kaum eine Software wird ihr gesamtes Leben vom ursprünglichen Autoren gepflegt.
-
Programmierungskonventionen verbessern die Lesbarkeit der Software, damit der Entwickler neuen Code schnell und vollständig verstehen kann.
-
Wenn wir uns entscheiden, den Quellcode den Mod-Community-Entwicklern freizugeben, möchten wir, dass er leicht verständlich ist.
-
Viele dieser Konventionen sind für die Compiler-übergreifende Kompatibilität erforderlich.
Die Programmierungsstandards unten sind C++-zentrisch. Der Standard ist jedoch zu befolgen, egal welche Sprache verwendet wird. Eine Sektion kann gleichwertige Regeln oder Ausnahmen für bestimmte Sprachen bieten, wo sie anwendbar sind.
Klassenorganisation
Klassen sollten so organisiert werden, dass sie den Leser und nicht den Schreiber im Blick behalten. Da die meisten Leser das öffentliche Interface der Klasse nutzen, sollte die öffentliche Implementierung zuerst deklariert werden, gefolgt von der privaten Implementierung der Klasse.
UCLASS()
class EXAMPLEPROJECT_API AExampleActor : public AActor
{
GENERATED_BODY()
public:
// Legt Default-Werte für die Eigenschaften dieses Actors fest
AExampleActor();
geschützt:
// wird aufgerufen, wenn das Spiel startet oder wenn gespawnt wird
virtual void BeginPlay() override;
};
Copyright-Hinweis
Jede Quelldatei (.h, .cpp, .xaml), die von Epic Games für öffentliche Verteilungen bereitgestellt wird, muss einen Copyright-Hinweis als erste Zeile in der Datei enthalten. Des Format des Verweises muss genau mit dem unten dargestellten Format übereinstimmen:
// Copyright Epic Games, Inc. Alle Rechte reserviert.
Wenn diese Zeile fehlt oder nicht richtig formatiert ist, erzeugt CIS einen Fehler und schlägt fehl.
Benennungskonventionen
Bei der Verwendung von Benennungskonventionen sollten alle Codes und Kommentare US-Englische Rechtschreibung und Grammatik verwenden.
- Der erste Buchstabe jedes Wortes in einem Namen (wie z. B. Typname oder Variablenname) wird großschreiben. Normalerweise gibt es keinen Unterstrich zwischen den Wörtern. Zum Beispiel sind
HealthundUPrimitiveComponentkorrekt,lastMouseCoordinatesoderdelta_coordinatesjedoch nicht.
Dies ist die PascalCase-Formatierung für Nutzer, die möglicherweise mit anderen objektorientierten Programmiersprachen vertraut sind
-
Um Typnamen von Variablennamen zu unterscheiden, wird ihnen ein zusätzlicher Großbuchstabe vorangestellt. Zum Beispiel ist
FSkinein Typname, undSkinist eine Instanz des TypsFSkin. -
Vorlagenklassen haben das Präfix T.
template <typename ObjectType> class TAttribute -
Klassen, die von UObject erben, haben das Präfix U.
class UActorComponent -
Klassen, die von AActor erben, haben das Präfix A.
class AActor -
Klassen, die von SWidget erben, haben das Präfix S.
class SCompoundWidget -
Klassen, die abstrakte Interfaces sind, haben das Präfix I.
class IAnalyticsProvider -
Den konzeptähnlichen Strukturtypen von Epic haben das Präfix C.
struct CStaticClassProvider { template <typename T> auto Requires(UClass*& ClassRef) -> decltype( ClassRef = T::StaticClass() ); }; -
Enums haben das Präfix E.
enum class EColorBits { ECB_Red, ECB_Green, ECB_Blue }; -
Boolesche Variablen müssen das Präfix b haben.
bPendingDestruction bHasFadedIn -
Die meisten anderen Klassen haben das Präfix F, obwohl einige Subsysteme andere Buchstaben verwenden.
-
Typedefs sollten mit einem Präfix versehen werden, was für diesen Typ geeignet ist, wie zum Beispiel:
-
F für typedef oder struct
-
U für typedef eines
UObject
-
-
Ein typedef einer bestimmten template-Instanziierung ist kein template mehr und sollte das entsprechende Präfix haben.
typedef TArray<FMytype> FArrayOfMyTypes; -
Präfixe werden in C# weggelassen.
-
Das Unreal Header Tool benötigt in den meisten Fällen das richtige Präfix, daher ist es wichtig, dieses anzugeben.
-
Typ-template-Parameter und geschachtelte Typ-Aliase basierend auf diesen template-Parametern unterliegen nicht den obigen Präfixregeln, da die Typkategorie unbekannt ist.
-
Bevorzugen Sie einen Typ-Suffix nach einem beschreibenden Begriff.
-
Vereindeutigen Sie template-Parameter von Aliasen durch Verwendung des Präfix In:
template <typename InElementType> class TContainer { public: using ElementType = InElementType; }; -
Typ- und Variablennamen sind Substantive.
-
Methodennamen sind Verben, die entweder den Effekt der Methode beschreiben, oder den Ergebniswert einer Methode ohne Effekt.
-
Makronamen sollten vollständig großgeschrieben werden, mit durch Unterstrichen getrennten Wörtern, und das Präfix
UE_haben.#define UE_AUDIT_SPRITER_IMPORT
Die Namen von Variablen, Methoden und Klassen sollten wie folgt aussehen:
-
Eindeutig
-
Eindeutig
-
Beschreibend
Je größer der Anwendungsbereich des Namens, desto wichtiger ist ein guter, beschreibender Name. Vermeiden Sie übermäßige Abkürzungen.
Alle Variablen sollten in ihrer eigenen Zeile deklariert werden, damit Sie einen Kommentar zur Bedeutung jeder Variablen geben können.
Der JavaDocs-Stil erfordert es.
Sie können mehrzeilige oder einzeilige Kommentare vor einer Variablen verwenden. Leere Zeilen sind optional für die Gruppierung von Variablen.
Alle Funktionen, die eine boolesche Variable zurückgeben, sollten eine True/False-Frage stellen, wie z. B. IsVisible() oder ShouldClearBuffer().
Eine Prozedur (eine Funktion ohne Ergebniswert) sollte ein starkes Verb verwenden, gefolgt von einem Objekt. Eine Ausnahme ist, wenn das Objekt der Methode das Objekt ist, in dem sie sich befindet. In diesen Fällen wird das Objekt aus dem Kontext heraus verstanden. Zu vermeiden sind Namen, die mit „Handle“ und „Process“ beginnen, da die Verben mehrdeutig sind.
Wir empfehlen Ihnen, Funktionsparameternamen mit dem Präfix „Out“ zu versehen, wenn:
-
Die Funktionsparameter per Referenz übergeben werden.
-
Es wird erwartet, dass die Funktion in diesen Wert schreibt.
So wird klar, dass der in diesem Argument übergebene Wert durch die Funktion ersetzt wird.
Wenn ein In- oder Out-Parameter auch ein boolescher Wert ist, setze "b" vor den In/Out-Präfix, z. B. bOutResult.
Funktionen, die einen Wert zurückgeben, sollten den Ergebniswert beschreiben. Aus dem Namen sollte klar hervorgehen, welchen Wert die Funktion zurückgibt. Dies ist besonders wichtig für Boolesche Funktionen. Sehen wir uns die folgenden zwei Beispielmethoden an:
// was bedeutet True?
bool CheckTea(FTea Tea);
// Der Name macht es klar, True bedeutet, dass der Tee frisch ist
bool IsTeaFresh(FTea Tea);
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
Inklusive Wortwahl
Wenn Sie mit der Unreal Engine-Codebasis arbeiten, empfehlen wir Ihnen, sich um die Verwendung einer respektvollen, integrativen und professionellen Sprache zu kümmern.
Wortwahl anwenden, wenn du:
-
Klassen
-
Funktionen
-
Datenstrukturen
-
Typen
-
Variablen
-
Dateien und Ordner.
-
Plugins benennen
Dies trifft zu, wenn Sie nutzerorientierte Snippets für die Benutzeroberfläche, Fehlermeldungen und Benachrichtigungen schreiben. Es wird auch beim Schreiben von Code angewendet, z. B. in Kommentaren und Änderungslistenbeschreibungen.
Die folgenden Abschnitte bieten Anleitungen und Vorschläge, um Ihnen zu helfen, Wörter und Namen zu wählen, die respektvoll und für alle Situationen und Publikum geeignet sind, und ein effektiverer Kommunikator sein.
Rassische, ethnische und religiöse Inklusivität
-
Verwenden Sie keine Metaphern oder Vergleiche, die Stereotypen verstärken. Beispiele umfassen Kontrast, schwarz und weiß oder blacklist und whitelist.
-
Verwenden Sie keine Wörter, die sich auf ein historisches Trauma oder ein gelebtes Erlebnis von Diskriminierung beziehen. Beispiele umfassen slave, master und nuke.
Einbeziehung der Geschlechter
-
Sie werden als they, them und their bezeichnet, sogar im Singular.
-
Beziehen Sie sich auf alles, was nicht eine Person ist, als it und its. Zum Beispiel, ein Modul, Plugin, Funktion, Client, Server, oder eine andere Software- oder Hardware-Komponente.
-
Weisen Sie nichts, was kein Geschlecht hat, einem Geschlecht zu.
-
Verwenden Sie keine Sammelbegriffe wie guys, die ein bestimmtes Alter annehmen.
-
Vermeiden Sie umgangssprachliche Phrasen, die willkürliche Generierungen enthalten, wie z. B. „a poor man's X“.
Slang
-
Denken Sie daran, dass Ihre Wörter von einem globalen Publikum gelesen werden, das möglicherweise nicht die gleichen Idiome und Einstellungen teilt und die möglicherweise nicht die gleichen kulturellen Referenzen versteht.
-
Vermeiden Sie Dialekte und umgangssprachliche Ausdrücke, auch wenn Sie sie für lustig oder harmlos halten. Diese können für Personen, deren erste Sprache nicht Englisch ist, schwer zu verstehen sein und sind möglicherweise nicht gut zu übersetzen.
-
Verwenden Sie keine Obszönitäten.
Überladene Wörter
- Viele Begriffe, die wir für ihre technischen Bedeutungen verwenden, haben auch andere Bedeutungen außerhalb von Technologie. Beispiele umfassen abort, execute oder native. Wenn Sie solche Wörter verwenden, sollten Sie immer präzise sein und den Kontext prüfen, in dem sie erscheinen.
Wortliste
Die folgende Liste, identifiziert einige Begriffe, die wir in der Vergangenheit in der Unreal-Codebasis verwendet haben, von denen wir aber denken, dass sie durch bessere Alternative ersetzt werden sollten:
| Wortname | Alternativer Wortname |
|---|---|
| Blacklist | _deny list_, _block list_, _exclude list_, _avoid list_, _unapproved list_, _forbidden list_,_permission list_ |
| Whitelist | allow list, include list, trust list, safe list, prefer list, approved list, permission list |
| Master | primary, source, controller, template, reference, main, leader, original, base |
| Slave | secondary, replica, agent, follower, worker, cluster node, locked, linked, synchronized |
Wir arbeiten aktiv daran, unseren Code mit den oben genannten Grundsätzen in Einklang zu bringen.
Portabler C++-Code
Die Typen int und unsigned int variieren in ihrer Größe je nach Plattform. Sie sind garantiert mindestens 32 Bit breit und sind in Code akzeptabel, wenn die Ganzzahl-Breite nicht wichtig ist. In serialisierten oder replizierten Formaten werden Typen mit expliziter Größe verwendet.
Unten finden Sie eine Liste gängiger Typen:
-
boolfür Boolesche Werte (Nehmen Sie NIEMALS die Größe von bool an).BOOLwird nicht kompiliert. -
TCHARfür einen Charakter (Nehmen Sie NIEMALS die Größe von TCHAR an). -
uint8für vorzeichenlose Bytes (1 Byte). -
int8für vorzeichenbehaftete Bytes (1 Byte). -
uint16für vorzeichenlose shorts (2 Bytes). -
int16für vorzeichenbehaftete shorts (2 Bytes). -
uint32für vorzeichenlose ints (4 Bytes). -
int32für vorzeichenbehaftete ints (4 Bytes). -
uint64für vorzeichenlose quad-Wörter (8 Bytes). -
int64für vorzeichenbehaftete Quad-Wörter (8 Bytes). -
floatfür Fließkomma mit einfacher Genauigkeit (4 Bytes). -
doublefür Fließkomma mit doppelter Genauigkeit (8 Bytes). -
PTRINTfür eine Ganzzahl, die einen Zeiger enthalten darf (Nehmen Sie NIEMALS die Größe von PTRINT an).
Nutzung der Standardbibliotheken
In der Vergangenheit hat UE aus folgenden Gründen die direkte Verwendung der C- und C++-Standardbibliotheken vermieden:
-
Ersetzen langsamer Implementierungen durch unsere eigenen, die zusätzliche Kontrolle über die Speicherallokation bereitstellen.
-
Neue Funktionen hinzufügen, bevor sie allgemein verfügbar sind, wie zum Beispiel:
-
Vornehmen erwünschter, aber nicht standardmäßiger Verhaltensänderungen.
-
Eine einheitliche Syntax in der gesamten Codebasis haben.
-
Vermeidung von Konstrukten, die mit den Idiomen von UE nicht kompatibel sind.
-
Die Standardbibliothek ist jedoch ausgereift und umfasst Funktionalität, die wir nicht mit einer Abstraktionsebene oder erneuten Implementierung selbst umhüllen möchten.
Wenn Sie zwischen einer Funktion der Standardbibliothek und unserer eigenen wählen können, sollten Sie die Option bevorzugen, die bessere Ergebnisse liefert. Es ist auch wichtig, sich daran zu erinnern, dass Konsistenz geschätzt wird. Wenn eine ältere UE-Implementierung ihren Zweck nicht mehr erfüllt, entscheiden wir uns möglicherweise dafür, sie abzuschaffen und die gesamte Verwendung auf die Standardbibliothek zu migrieren.
Vermeiden Sie das Vermischen von UE-Idiomen und Standardbibliotheks-Idiomen in der gleichen API. In der folgenden Tabelle werden gebräuchliche Idiome sowie Empfehlungen zu ihrer Verwendung aufgeführt.
| Idiom | Beschreibung |
|---|---|
<atomic> |
Das Atomic-Idiom sollte in neuem Code verwendet und alter Code bei Bedarf migriert werden. Atomics sollten auf allen unterstützten Plattformen vollständig und effizient implementiert sein. Unser eigenes TAtomic ist nur teilweise implementiert und es liegt nicht in unserem Interesse, es zu pflegen und zu verbessern. |
<type_traits> |
Das Type-Traits-Idiom sollte verwendet werden, wenn es eine Überlappung zwischen einem alten UE-Merkmal und einem Standardmerkmal gibt. Merkmale werden aus Gründen der Korrektheit häufig als Compiler-Intrinsics implementiert, und Compiler können die Standardmerkmale kennen und schnellere Kompilierungspfade auswählen, anstatt sie als reines C++ zu behandeln. Ein Problem ist, dass unsere Merkmale normalerweise einen großgeschriebenen statischen Value oder Type-Typ haben, während Standardmerkmale value und type verwenden sollen. Dies ist ein wichtiger Unterschied, da von Kompositionsmerkmalen eine bestimmte Syntax erwartet wird, z. B. std::conjunction. Neue Merkmale, die wir hinzufügen, sollten mit kleingeschriebenem value oder type geschrieben werden, um den Aufbau zu unterstützen. Vorhandene Merkmale sollten aktualisiert werden, um beide Fälle zu unterstützen. |
<initializer_list> |
Das Idiom Initialisierungsliste muss verwendet werden, um die Initialisierersyntax in geschweiften Klammern zu unterstützen. Dies ist ein Fall, in dem sich die Sprache und die Standardbibliotheken überlappen. Es gibt keine Alternative, wenn Sie dies unterstützen möchten. |
<regex> |
Das Regex-Idiom kann direkt verwendet werden, aber seine Verwendung sollte im Editor-exklusiven Code gekapselt sein. Wir haben keine Pläne, unsere eigene Regex-Lösung zu implementieren. |
<limits> |
std::numeric_limits kann in Gänze verwendet werden. |
<cmath> |
Alle Fließkomma-Funktionen aus diesem Header können verwendet werden. |
<cstring>: memcpy() und memset() |
Diese Idiome können anstelle von FMemory::Memcpy bzw. FMemory::Memset verwendet werden, wenn sie einen nachweisbaren Performance-Vorteil haben. |
Standard-Container und Strings sollten vermieden werden, außer im Interop-Code.
Kommentare
Kommentare sind Kommunikation und Kommunikation ist wichtig. In den folgenden Abschnitten werden einige wichtige Dinge aufgeführt, die Sie über Kommentare beachten sollten (aus Kernighan & Pike The Practice of Programming).
Richtlinien
-
Schreibe selbstdokumentierenden Code. Zum Beispiel:
// Schlecht: t = s + l - b; // Gut: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Schreiben Sie nützliche Kommentare. Zum Beispiel:
// Schlecht: // Blätter inkrementieren ++Leaves; // Gut: // wir wissen, dass es ein weiteres Teeblatt gibt ++Leaves; -
Überkommentieren Sie schlechten Code nicht - schreiben Sie ihn stattdessen um. Zum Beispiel:
// Schlecht: // Die Gesamtzahl der Blätter ist die Summe von // kleinen und großen Blättern weniger als die // Anzahl der Blätter, die beides sind t = s + l - b; // Gut: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Widersprechen Sie nicht dem Code. Zum Beispiel:
// Schlecht: // Blätter niemals inkrementieren! ++Leaves; // Gut: // wir wissen, dass es ein weiteres Teeblatt gibt ++Leaves;
const-Korrektheit
const ist sowohl eine Dokumentation als auch eine Compiler-Direktive. Jeder Code sollte danach streben, const-korrekt zu sein. Dies umfasst die folgenden Richtlinien:
-
Übergeben Sie Funktions-Argumente per const-Zeiger oder Referenz, wenn diese Argumente nicht durch die Funktion geändert werden sollen.
-
Markieren Sie Methoden als const, wenn sie das Objekt nicht modifizieren.
-
Verwenden Sie die const-Iteration über Container, wenn die Schleife den Container nicht verändern soll.
const-Beispiel:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray wird hier nicht geändert, aber OutResult wahrscheinlich schon
}
void FThing::SomeNonMutatingOperation() const
{
// Dieser Code ändert das FThing, auf dem er aufgerufen wird, nicht
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// Der Körper dieser Schleife modifiziert StringArray nicht
}
const wird auch für per-Wert-Funktionsparameter und lokale Werte bevorzugt. So wird dem Leser mitgeteilt, dass die Variable im Körper der Funktion nicht verändert wird, was sie verständlicher macht. Wenn Sie dies tun, stellen Sie sicher, dass die Deklaration und die Definition übereinstimmen, da dies sich auf die JavaDoc-Verarbeitung auswirken kann.
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Weder Count noch CountPlusOne können während das Ausführung des Körpers der Funktion geändert werden
}
Eine Ausnahme bilden per Wert übergebene Parameter, die in einen Container verschoben werden. Weitere Informationen finden Sie im Abschnitt „Bewegungssemantik“ auf dieser Seite.
Beispiel:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
Setzt das Schlüsselwort „const“ an das Ende, wenn ein Zeiger selbst zu einem const gemacht wird (und nicht das, worauf er zeigt). Referenzen können ohnehin nicht „neu zugewiesen“ werden und können daher nicht auf die gleiche Weise zu „const“ gemacht werden.
Beispiel:
// const-Zeiger zu nicht-const-Objekt – Zeiger kann nicht neu zugewiesen werden, aber T kann noch geändert werden
T* const Ptr = ...;
// Ungültig
T& const Ref = ...;
Verwenden Sie niemals const für einen Rückgabe-Typ. Dies verhindert die Bewegungssemantik für komplexen Typen und gibt Kompilierwarnungen für integrierte Typ aus. Diese Regel gilt nur für den Rückgabe-Typ selbst, nicht für den Zieltyp eines Zeiger- oder Referenztyps, der zurückgegeben wird.
Beispiel:
// Schlecht – Rückgabe eines const-Array
const TArray<FString> GetSomeArray();
// Fein – Rückgabe einer Referenz auf ein const-Array
const TArray<FString>& GetSomeArray();
// Fein – Rückgabe eines Zeigers an ein const-Array
const TArray<FString>* GetSomeArray();
// Schlecht – Rückgabe eines const-Zeigers an ein const-Array
const TArray<FString>* const GetSomeArray();
Beispiel-Formatierung
Wir verwenden ein System basierend auf JavaDoc, um Kommentare aus dem Code zu extrahieren und die Dokumentation automatisch zu erstellen. Daher empfehlen wir spezielle Kommentar-Formatierungsregeln.
Das folgende Beispiel zeigt die Formatierung von Kommentaren für class, method und variable. Denken Sie daran, dass Kommentare den Code erweitern sollten. Code dokumentiert die Implementierung, während Kommentare die Absicht dokumentieren. Stellen Sie sicher, dass Sie Kommentare aktualisieren, wenn Sie die Absicht eines Codeabschnitts ändern.
Beachten Sie, dass zwei verschiedene Parameterkommentarstile unterstützt werden, die durch die Methoden Steep und Sweeten angezeigt werden. Der von Steep verwendete Stil @param ist der traditionelle Mehrzeilenstil. Bei einfachen Funktionen kann es übersichtlicher sein, die Parameter- und Ergebniswertdokumentation in den beschreibenden Kommentar zur Funktion zu integrieren. Dies wird im Beispiel Sweeten demonstriert. Spezielle Kommentar-Tags wie @see Oder @return sollten nur verwendet werden, um neue Zeilen zu beginnen, die auf die primäre Beschreibung folgen.
Methoden-Kommentare sollten nur einmal enthalten sein: dort, wo die Methode öffentlich deklariert wird. Die Methoden-Kommentare sollte nur Informationen enthalten, die für den Aufrufer der Methode relevant sind, einschließlich aller Informationen über das Überschreiben der Methode, die für den Aufrufer relevant sein können. Details zur Implementierung der Methode und ihrer Überschreibungen, die für Aufrufer nicht relevant sind, sollten innerhalb der Implementierung der Methode erläutert werden.
Klassen-Kommentare sollten Folgendes umfassen:
-
Eine Beschreibung des Problems, das diese Klasse löst.
-
Der Grund, warum diese Klasse erstellt wurde.
Die mehrzeiligen Methoden-Kommentare sollten Folgendes umfassen:
-
Funktionszweck: Dokumentiert das Problem, das diese Funktion löst. Wie bereits erwähnt, dokumentieren Kommentare die Absicht und Code die Implementierung.
-
**Parameterkommentar: Jeder Parameterkommentar sollte umfassen:
-
Maßeinheit
-
der Bereich der erwarteten Werte
-
„unmögliche“ Werte
-
und die Bedeutung von Status-/Fehlercodes.
-
-
**Ergebnis-Kommentar: Dokumentiert den erwarteten Ergebniswert, so wie eine Output-Variable dokumentiert wird. Um Redundanz zu vermeiden, sollte kein expliziter
@return-Kommentar verwendet werden, wenn der einzige Zweck der Funktion darin besteht, diesen Wert zurückzugeben und dies bereits im Funktionszweck dokumentiert ist. -
Zusätzliche Informationen:
@warning,@note,@seeund@deprecatedkönnen optional verwendet werden, um zusätzliche relevante Informationen zu dokumentieren. Jeder sollte in seiner eigenen Zeile im Anschluss an den Rest des Kommentars deklariert werden.
Moderne C++-Sprachsyntax
Unreal Engine wurde so konzipiert, dass sie extrem auf viele C++-Compiler portabel ist. Daher achten wir darauf, Funktionen zu verwenden, die mit den Compilern kompatibel sind, die wir möglicherweise unterstützen. Manchmal sind Funktionen so nützlich, dass wir sie in Makros verpacken und sie überall verwenden. Normalerweise warten wir jedoch, bis alle von uns unterstützten Compiler auf dem neuesten Standard sind.
Unreal Engine kompiliert standardmäßig mit einer Sprachversion von C++20 und erfordert zum Erstellen eine Mindestversion von C++20. Wir verwenden viele moderne Sprachfunktionen, die über moderne Compiler gut unterstützt werden. In einigen Fällen verpacken wir die Verwendung dieser Funktionen in Präprozessor-Bedingungen. Manchmal entscheiden wir uns jedoch aus Gründen der Portabilität oder aus anderen Gründen, bestimmte Sprachfunktionen ganz zu vermeiden.
Sofern unten nicht anders angegeben, sollten Sie als von uns unterstützte moderne C++-Compilerfunktion keine Compiler-spezifischen Sprachfunktionen verwenden, es sei denn, sie sind in Präprozessormakros oder Bedingungen gekapselt und werden sparsam eingesetzt.
Statisches Assert
Das Schlüsselwort static_assert ist gültig für die Verwendung, wenn Sie eine Assertion zur Kompilierzeit benötigen.
override und final
Die Schlüsselwörter override und final sind gültig für die Verwendung, und ihre Verwendung wird dringend empfohlen. Es könnte viele Stellen geben, an denen diese ausgelassen wurden, aber sie werden mit der Zeit behoben.
nullptr
Sie sollten in allen Fällen nullptr anstelle des C-Stil-Makros NULL verwenden.
Eine Ausnahme ist die Verwendung von nullptr in C++/CX-Builds (z. B. für Xbox One). In diesen Fällen ist die Verwendung von nullptr eigentlich der verwaltete Null-Referenztyp. Er ist größtenteils mit nullptr aus nativem C++ kompatibel, außer in seinem Typ und einigen Vorlage-Instanziierungskontexten. Daher sollten Sie aus Kompatibilitätsgründen das Makro TYPE_OF_NULLPTR anstelle des üblicheren decltype(nullptr) verwenden.
Auto
Sie sollten auto nicht in C++-Code verwenden, mit Ausnahme der wenigen Ausnahmen, die unten aufgeführt sind. Geben Sie immer explizit an, welchen Typ Sie initialisieren. Das bedeutet, dass der Typ für den Leser deutlich sichtbar sein muss. Diese Regel gilt auch für die Verwendung des Schlüsselworts var in C#.
Die strukturierte Bindungsfunktion von C++20 sollte ebenfalls nicht verwendet werden, da sie praktisch ein variadisches auto ist.
Akzeptable Verwendung von auto:
-
Wenn Sie ein Lambda an eine Variable binden müssen, da Lambda-Typen im Code nicht ausdrückbar sind.
-
Für Iterator-Variablen. Aber nur, wenn der Typ des Iterators ausführlich ist und die Lesbarkeit beeinträchtigen würde.
-
In Vorlagen-Code, wo der Typ eines Ausdrucks nicht leicht zu erkennen ist. Dies ist ein erweiterter Fall.
Es ist sehr wichtig, dass Typen für jemand, der den Code liest, deutlich sichtbar sind. Auch wenn einige IDEs in der Lage sind, den Typ abzuleiten, hängt dies davon ab, ob sich der Code in einem kompilierbaren Zustand befindet. Es hilft auch nicht den Nutzern von merge/diff-Werkzeugen, oder wenn einzelne Quell-Dateien isoliert angezeigt werden, wie z. B. auf GitHub.
Wenn Sie sicher sind, dass Sie auto auf eine akzeptable Weise verwenden, denken Sie immer daran, const, & oder * richtig zu verwenden, genau wie Sie es bei dem Typnamen machen würden. Mit auto wird der abgeleitete Typ auf den gewünschten Typ festgelegt.
Bereichsbasiertes „for“
Dies wird bevorzugt, damit der Code leichter verständlich und wartbar bleibt. Wenn Sie Code migrieren, der alte TMap-Iteratoren verwendet, sollte Ihnen bewusst sein, dass die alten Funktionen Key und Value(), die Methoden des Iterator-Typs waren, jetzt einfach die Felder Key und Value des zugrunde liegenden Wertepaares TPair sind.
Beispiel:
TMap<FString, int32> MyMap;
// Alter Stil
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// Neuer Stil
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
Wir haben auch Bereichsersetzungen für einige eigenständig Iterator-Typen.
Beispiel:
// Alter Stil
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// Neuer Stil
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
Lambdas und anonyme Funktionen
Lambdas können frei verwendet werden, bringen aber zusätzliche Sicherheitsprobleme mit sich. Die besten Lambdas sollten nicht mehr als ein paar Anweisungen in der Länge sein, insbesondere wenn sie als Teil eines größeren Ausdrucks oder einer Anweisung verwendet werden, zum Beispiel als Prädikat in einem generischen Algorithmus.
Beispiel:
// Finde die erste Sache, deren Name das Wort „Hallo“ enthält
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT(„Hallo")); });
// Array in umgekehrter Reihenfolge des Namens sortieren
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });|
Seien Sie sich bewusst, dass zustandsbehaftete Lambdas dem Funktions-Zeiger nicht zugewiesen werden können, was wir am häufigsten verwenden. Nicht-triviale Lambdas sollten auf die gleiche Weise dokumentiert werden wie reguläre Funktionen. Lambdas können auch als Delegates für zurückgestellte Ausführung mit Funktionen wie BindWeakLambda verwendet werden, wobei die Variablenfunktion als Nutzlast erfasst wird.
Datenbindungen (Captures) und Rückgabe-Typen
Anstelle der automatischen Datenbindung ([&] und [=]) sollten explizite Datenbindungen verwendet werden. Dies ist aus Gründen der Lesbarkeit, Wartbarkeit, Sicherheit und Performance wichtig, insbesondere bei großen Lambdas und zurückgestellter Ausführung.
Explizite Datenbindungen erklären die Absicht des Autoren. Daher werden Fehler bei Codeüberprüfungen abgefangen. Falsche Datenbindungen können zu erheblichen Bugs und Abstürzen führen, die mit höherer Wahrscheinlichkeit problematisch werden, wenn der Code im Laufe der Zeit gewartet wird. Hier sind einige zusätzliche Dinge, die Sie bei Lambda-Datenbindungen beachten sollten:
-
Die per-Referenz-Datenbindung und die per-Wert-Datenbindung von Zeigern (einschließlich des
This-Pointer) können zu Datenbeschädigungen und Abstürzen führen, wenn die Ausführung des Lambdas zurückgestellt wird. Lokale Variablen und Membervariablen sollten für zurückgestellte Lambdas niemals per Referenz erfasst werden. -
Die Datenbindung per Wert kann ein Performance-Problem darstellen, wenn sie unnötige Kopien für ein nicht zurückgestelltes Lambda erstellt.
-
Versehentlich erfasste UObject-Zeiger sind für den Garbage Collector unsichtbar. Die automatische Datenbindung fängt
thisimplizit ab, wenn auf irgendwelche Member-Variablen referenziert wird, obwohl[=]den Eindruck erweckt, dass das Lambda seine eigenen Kopien von allem hat. -
Delegat-Wrapper wie
CreateWeakLambdaundCreateSPLambdasollten für die zurückgestellte Ausführung verwendet werden, da die Bindung automatisch aufgehoben wird, wenn UObject oder der gemeinsame Zeiger freigegeben werden. Andere gemeinsame Objekte können als TWeakObjectPtr oder TWeakPtr erfasst und dann innerhalb des Lambdas validiert werden. -
Jede zurückgestellte Lambda-Verwendung, die diesen Richtlinien nicht folgt, muss einen Kommentar haben, der erklärt, warum die Alpha-Datenbindung sicher ist.
Explizite Rückgabe-Typen sollten für große Lambdas verwendet werden oder wenn Sie das Ergebnis eines anderen Funktionsaufrufs zurückgeben. Diese sollten auf die gleiche Weise wie das Schlüsselwort „auto“ berücksichtigt werden.
Stark typisierte Enums
Enumerierte (Enum-) Klassen sind ein Ersatz für altmodische Enums mit Namensraum, sowohl für reguläre Aufzählungen als auch für UENUMs. Zum Beispiel:
// Alte Enum
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// Neue Enum
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
Enums werden als UPROPERTYs unterstützt und ersetzen die alte Übergangslösung TEnumAsByte<>. Die Enum-Eigenschaften können auch eine beliebige Größe haben, nicht nur Bytes:
/ Alte Eigenschaft
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// Neue Eigenschaft
UPROPERTY()
EThing MyProperty;
Für Blueprints verfügbare Enums müssen weiterhin auf „uint8“ basieren.
Enum-Klassen, die als Flag verwendet werden, können das Makro ENUM_CLASS_FLAGS(EnumType) nutzen, um automatisch alle bitweisen Operatoren zu definieren:
enum class EFlags
{
None = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
Die einzige Ausnahme ist die Verwendung von Flags in einem truth-Kontext – dies ist eine Beschränkung der Sprache. Stattdessen sollten alle Enum-Flags einen Enumerator namens None haben, der für Vergleiche auf 0 gesetzt wird:
// Alt
if (Flags & EFlags::Flag1)
// Neu
if ((Flags & EFlags::Flag1) != EFlags::None)
Bewegungssemantik
Alle der wichtigsten Containertypen – TArray, TMap, TSet, FString – haben Bewegungskonstruktor und Bewegungszuweisungsoperatoren. Diese werden oft automatisch verwendet, wenn diese Typen per Wert übergeben oder zurückgegeben werden. Sie können auch explizit aufgerufen werden, indem MoveTemp verwendet wird, das UE-Äquivalent von std::move.
Das Zurückgeben von Containern oder Strings per Wert kann für die Ausdruckskraft vorteilhaft sein, ohne die üblichen Kosten für temporäre Kopien. Regeln für die Wertübergabe und die Verwendung von MoveTemp werden noch festgelegt, sind aber bereits in einigen optimierten Bereichen der Codebasis zu finden.
Default-Member-Initialisierer
Default-Member-Initialisierer können verwendet werden, um die Standardeinstellungen einer Klasse innerhalb der Klasse selbst zu definieren:
UCLASS()
class UTeaOptions : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
int32 MaximumNumberOfCupsPerDay = 10;
UPROPERTY()
float CupWidth = 11.5f;
UPROPERTY()
FString TeaType = TEXT("Earl Grey");
UPROPERTY()
EDrinkingStyle DrinkingStyle = EDrinkingStyle::PinkyExtended;
};
So geschriebener Code hat die folgenden Vorteile:
-
Es ist nicht erforderlich, Initialisierer über mehrere Konstruktoren hinweg zu duplizieren.
-
Es ist nicht möglich, die Initialisierungsreihenfolge und die Deklarationsreihenfolge zu vermischen.
-
Der Member-Typ, die Eigenschafts-Flags und die Default-Werte befinden sich alle an einem Ort. Dies hilft der Lesbarkeit und Wartbarkeit.
Es gibt jedoch auch einige Schattenseiten:
-
Jede Änderung der Standardeinstellungen erfordert eine Neuerstellung aller abhängigen Dateien.
-
Header können in Patch-Releases der Engine nicht geändert werden. Daher kann dieser Stil die möglichen Korrekturen einschränken.
-
Einige Dinge können nicht auf diese Weise initialisiert werden, wie z. B. Basis-Klassen,
UObject-Teilobjekte, Zeiger auf vorwärtsdeklarierte Typen, aus Konstruktorargumenten abgeleitete Werte und über mehrere Schritte initialisierte Members. -
Einige der Initialisierer in den Header und den Rest in den Konstruktor in der .cpp-Datei einfügen, kann die Lesbarkeit und Wartbarkeit verringern.
Gehen Sie nach bestem Wissen und Gewissen vor, wenn Sie entscheiden, ob Sie den Default-Member-Initialisierer verwenden möchten. Als Faustregel gilt, dass Default-Member-Initialisierer mit In-Game-Code mehr Sinn machen als mit Engine-Code. Erwägen Sie, Konfigurationsdateien für Default-Werte zu verwenden.
Code von Drittanbietern
Wenn Sie den Code in einer Bibliothek ändern, die wir in der Engine verwenden, kennzeichnen Sie Ihre Änderungen unbedingt mit einem Kommentar //@UE5 sowie einer Erklärung, warum Sie die Änderung vorgenommen haben. Das macht das Zusammenführen der Änderungen in einer neuen Version dieser Bibliothek einfacher und stellt sicher, dass der Lizenznehmer alle von uns vorgenommenen Änderungen leicht finden kann.
Jeder in die Engine eingebundene Code von Drittanbietern sollte mit Kommentaren versehen sein, die so formatiert sind, dass sie leicht durchsuchbar sind. Zum Beispiel:
// @Drittanbieter-Code - PhysX BEGINNEN
#include <physx.h>
// @Drittanbieter-Code - PhysX ENDE
// @Drittanbieter-Code - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
// Wird verwendet, um den Thread-Namen im Debugger festzulegen
…
//@Drittanbieter-Code - END MSDN SetThreadName
Codeformatierung
geschweifte Klammern
Klammerkriege sind übel. Epic Games hat ein lange bewährtes Verwendungsmuster, um geschweifte Klammern in eine neue Zeile zu setzen. Bitte halten Sie sich an diese Verwendung, unabhängig von der Größe der Funktion oder des Blocks. Zum Beispiel:
// Schlecht
int32 GetSize() const { return Size; }
// Gut
int32 GetSize() const
{
return Größe;
}
Umschließen Sie Blöcke mit einer Anweisung immer mit geschweiften Klammern. Zum Beispiel:
if (bThing)
{
return;
}
If - else
Jeder Block der Ausführung in einer if-else-Anweisung sollte in geschweiften Klammern stehen. Diese hilft Bearbeitungsfehler zu verhindern. Wenn keine geschweifte Klammern verwendet werden, könnte jemand unabsichtlich eine weitere Zeile zu einem if-Block hinzufügen. Die zusätzliche Zeile würde nicht durch den if-Ausdruck gesteuert, was schlecht wäre. Es ist auch schlecht, wenn bedingt kompilierte Elemente dazu führen, dass if/else-Anweisungen nicht mehr funktionieren. Verwenden Sie also immer geschweifte Klammern.
if (bHaveUnrealLicense)
{
InsertYourGameHere();
}
else
{
CallMarkRein();
}
Eine Mehrweg-if-Anweisung sollte so eingerückt werden, dass jedes else if um den gleichen Betrag eingerückt wird wie das erste if. Dadurch wird die Struktur für den Leser deutlich:
if (TannicAcid < 10)
{
UE_LOG(LogCategory, Log, TEXT("Low Acid"));
}
else if (TannicAcid < 100)
{
UE_LOG(LogCategory, Log, TEXT("Medium Acid"));
}
else
{
UE_LOG(LogCategory, Log, TEXT("High Acid"));
}
Tabulatoren und Einrückung
Im Folgenden finden Sie einige Standards für die Einrückung Ihres Codes.
-
Rücken Sie den Code nach Ausführungsblock ein.
-
Verwenden Sie Tabulatoren für Leerräume am Anfang einer Zeile, keine Leerzeichen. Legen Sie Ihren Tabulator auf die Größe von 4 Zeichen fest. Beachten Sie, dass Leerzeichen manchmal notwendig und erlaubt sind, um die Ausrichtung des Codes unabhängig von der Anzahl der Leerzeichen in einem Tabulator aufrechtzuerhalten. Zum Beispiel, wenn Sie Code ausrichten, der auf Zeichen folgt, die keine Tabulatoren sind.
-
Wenn Sie Code in C# schreiben, verwenden Sie bitte auch Tabulatoren, keine Leerzeichen. Der Grund dafür ist, dass Programmierer oft zwischen C# und C++ wechseln und die meisten es vorziehen, eine einheitliche Einstellung für Tabulatoren zu verwenden. Visual Studio verwendet standardmäßig Leerzeichen für C#-Dateien. Sie müssen also daran denken, diese Einstellung zu ändern, wenn Sie mit Code in Unreal Engine arbeiten.
Switch-Anweisungen
Mit Ausnahme von leeren Fällen (mehrere Fälle mit identischem Code) sollten switch-case-Anweisungen explizit kennzeichnen, dass ein Fall in den nächsten Fall übergeht. Fügen Sie entweder jeweils ein break ein oder fügen Sie einen „fällt durch“-Kommentar ein. Andere Code-Steuerungsübertragungsbefehle (return, continue usw.) sind ebenfalls in Ordnung.
Haben Sie immer einen „default“-Fall. Fügen Sie ein „break“ ein, für den Fall, dass jemand nach dem „default“ einen neuen Fall hinzufügt.
switch (condition)
{
case 1:
…
// fällt durch
case 2:
…
break;
case 3:
…
return;
case 4:
case 5:
…
break;
default:
break;
}
Namespaces
Sie können Namespaces verwenden, um Ihre Klassen, Funktionen und Variablen nach Bedarf zu organisieren. Wenn Sie sie verwenden, folgen sie den Regeln unten.
-
Der meiste UE-Code ist derzeit nicht in einen globalen Namespace gepackt.
- Achten Sie darauf, Kollisionen im globalen Bereich zu vermeiden, insbesondere wenn Sie Code von Drittanbietern verwenden oder einbinden.
-
Namespaces werden von UnrealHeaderTool nicht unterstützt.
- Namespaces sollten nicht bei der Definition von
UCCLASSes,USTRUCTsusw. verwendet werden.
- Namespaces sollten nicht bei der Definition von
-
Neue APIs, die nicht
UCLASSes,USTRUCTsusw. sind, sollten in einemUE::-Namespace platziert werden, und idealerweise in einem Namespace, z. BUE Audio::geschachtelt werden.- Namespaces, die zur Speicherung von Implementierungsdetails verwendet werden, die nicht Teil der öffentlich zugänglichen API sind, sollten in einen
Private-Namespace verschoben werden, z. BUE::Audio::Private::.
- Namespaces, die zur Speicherung von Implementierungsdetails verwendet werden, die nicht Teil der öffentlich zugänglichen API sind, sollten in einen
-
Using-Deklarationen:- Platzieren Sie keine
using-Deklarationen im globalen Bereich, auch nicht in einer.cpp-Datei (dies würde Probleme mit unserem „Unity“-Build-System verursachen).
- Platzieren Sie keine
-
Es ist in Ordnung,
using-Deklarationen innerhalb eines anderen Namespace oder innerhalb eines Funktionskörpers zu platzieren. -
Wenn Sie
using-Deklarationen in einen Namespace einfügen, wird dies auf andere Vorkommen dieses Namespaces in derselben Übersetzungseinheit übertragen. So lange Sie konsequent sind, ist es in Ordnung. -
Sie können die
using-Deklaration in Header-Dateien nur dann sicher verwenden, wenn Sie die obigen Regeln befolgen. -
Vorwärtsdeklarierte Typen müssen innerhalb ihres jeweiligen Namespace deklariert werden.
- Wenn Sie dies nicht machen, werden Sie Link-Fehler bekommen.
-
Wenn Sie viele Klassen oder Typen innerhalb eines Namespace deklarieren, kann es schwierig sein, diese Typen in anderen Klassen mit globalem Gültigkeitsbereich zu verwenden (Funktionssignaturen müssen beispielsweise explizite Namespaces verwenden, wenn sie in Klassendeklarationen erscheinen).
-
Sie können
using-Deklarationen verwenden, um nur bestimmten Variablen innerhalb eines Namespace Aliase in Ihren Geltungsbereich zuzuweisen.- Zum Beispiel mit
Foo::FBar. Aber normalerweise machen wir das im Unreal-Code nicht.
- Zum Beispiel mit
-
Makros können sich nicht in einem Namespace befinden.
- Sie sollten das Präfix
UE_haben, anstatt sich in einem Namespace zu befinden, zum BeispielUE_LOG.
- Sie sollten das Präfix
Physische Abhängigkeiten
-
Dateinamen sollte nach Möglichkeit nicht mit einem Präfix versehen werden.
- Zum Beispiel:
Scene.cppanstelle vonUScene.cpp. Dies erleichtert die Verwendung von Werkzeugen wie „Workspace Whiz“ oder „Open File in Solution“ von Visual Assist, indem die Anzahl der Buchstaben reduziert wird, die zur Identifizierung der gewünschten Datei benötigt werden.
- Zum Beispiel:
-
Alle Header sollten mit der Direktive
#pragma oncevor mehrfachen „includes“ geschützt werden.-
Beachten Sie, dass alle Compiler, die wir verwenden,
#pragma onceunterstützen.#pragma once //<file contents>
-
-
Versuchen Sie, die physische Kopplung zu minimieren.
- Vermeiden Sie insbesondere das Einbeziehen der Standardbibliothek-Header aus anderen Header.
-
Vorwärts-Deklarationen sind dem Einbinden von Headern vorzuziehen.
-
Wenn Sie einen Header einfügen, sollten Sie so feinkörnig wie möglich sein.
- Fügen Sie beispielsweise nicht
Core.hein. Stattdessen sollten Sie die spezifischen Header in „Core“ einfügen, aus denen Sie Definitionen benötigen.
- Fügen Sie beispielsweise nicht
-
Versuchen Sie, jeden benötigten Header direkt einzufügen, um die feinkörnige Einbindung einfacher zu machen.
-
Verlassen Sie sich nicht auf einen Header, der indirekt von einem anderen Header, den Sie einfügen, enthalten ist.
-
Verlassen Sie sich nicht darauf, dass etwas über einen anderen Header eingefügt wird. Fügen Sie alles ein, was Sie benötigen.
-
Module haben „Private“- und „Public“-Quellverzeichnisse.
- Alle Definitionen, die von anderen Modulen benötigt werden, müssen in den Headern im Verzeichnis „Public“ stehen. Alles andere sollte im Verzeichnis „Private“ stehen. In älteren Unreal-Modulen können diese Verzeichnisse noch „Src“ und „Inc“ heißen, aber diese Verzeichnisse sollen privaten und öffentlichen Code auf die gleiche Weise trennen und nicht dazu, Header-Dateien von Quelldateien zu trennen.
-
Machen Sie sich keine Gedanken über das Einrichten Ihrer Header für die vorkompilierte Header-Generierung.
- UnrealBuildTool kann diesen Job besser machen als Sie.
-
Aufteilen großer Funktionen in logische Teil-Funktionen.
- Ein Bereich der Compileroptimierungen ist die Eliminierung gemeinsamer Teilausdrücke. Je größer Ihre Funktion ist, desto mehr Arbeit muss der Compiler leisten, um sie zu identifizieren. Dies führt zu erheblich längeren Build-Zeiten.
-
Verwenden Sie keine große Anzahl von Inline-Funktionen.
- Inline-Funktionen erzwingen Neuerstellungen, auch in Dateien, die sie nicht verwenden. Inline-Funktionen sollten nur für triviale Zugriffsmethoden verwendet werden und nur dann, wenn sich beim Profiling zeigt, dass dies von Vorteil ist.
-
Seien Sie konservativ bei der Verwendung von
FORCEINLINE.- Alle Codes und lokalen Variablen werden in die aufrufende Funktion erweitert. Dies verursacht die gleichen Build-Zeitprobleme wie eine große Funktion.
Kapselung
Erzwingen Sie die Verkapselung mit den protection-Schlüsselwörtern. Klassen-Members sollten fast immer als „private“ deklariert werden, es sei denn, sie sind Teil des Interface public/protected zur Klasse. Gehen Sie nach bestem Wissen und Gewissen vor, aber seien Sie sich immer darüber im Klaren, dass ein Mangel an Zugriffsmethoden ein späteres Umgestalten erschwert, ohne Plugins und vorhandene Projekte zu beschädigen.
Wenn bestimmte Felder nur durch abgeleitete Klassen anwendbar sein sollen, machen Sie sie „private“ und stellen Sie „protected“ Zugriffsmethoden bereit.
Verwenden Sie „final“, wenn Ihre Klasse nicht abgeleitet werden soll.
Allgemeine Stilprobleme
-
Minimieren der Abhängigkeitsdistanz.
- Wenn Code von einer Variable mit einem bestimmten Wert abhängt, versuchen Sie, den Wert dieser Variablen festzulegen, bevor Sie sie verwenden. Eine Variable am Anfang eines Ausführungsblocks zu initialisieren und sie für hundert Zeilen Code nicht zu verwenden, bietet viel Raum für jemand, der versehentlich den Wert ändert, ohne die Abhängigkeit zu erkennen. Wenn sie in der nächsten Zeile steht, wird klar, warum die Variable initialisiert wird, wie sie ist und wo sie verwendet wird.
-
Teilen Sie Methoden in Untermethoden auf, wo immer es möglich ist.
- Es ist einfacher für jemand, sich ein großes Bild anzusehen und dann Details zu den interessanten Details anzuzeigen, als mit den Details zu beginnen und das große Bild aus ihnen zu rekonstruieren. In der gleichen Weise ist es einfacher, eine einfache Methode zu verstehen, die eine Sequenz von mehreren gut benannten Untermethoden aufruft, als eine äquivalente Methode zu verstehen, die einfach den gesamten Code in diesen Untermethoden enthält.
-
Fügen Sie in Funktionsdeklarationen oder Funktionsaufrufstellen kein Leerzeichen zwischen dem Funktionsnamen und den Klammern ein, die der Argumentliste vorangehen.
-
Beheben Sie Compilerwarnungen.
- Warnmeldungen des Compilers bedeuten, dass etwas nicht stimmt. Beheben Sie das Problem, vor dem der Compiler Sie warnt. Wenn Sie es absolut nicht beheben können, verwenden Sie
#pragma, um die Warnung zu unterdrücken, aber das sollten Sie nur als letzten Ausweg tun.
- Warnmeldungen des Compilers bedeuten, dass etwas nicht stimmt. Beheben Sie das Problem, vor dem der Compiler Sie warnt. Wenn Sie es absolut nicht beheben können, verwenden Sie
-
Lassen Sie eine leere Zeile am Ende der Datei.
- Alle
.cpp- und.h-Dateien sollten zur Koordination mit gcc eine Leerzeile enthalten.
- Alle
-
Debug-Code sollte entweder nützlich und ausgefeilt sein oder nicht eingecheckt werden.
- Mit anderem Code vermischter Debug-Code erschwert die Lesbarkeit des anderen Codes.
-
Verwenden Sie für String-Literale immer das Makro „TEXT()“.
- Ohne das Makro
TEXT()führt Code, derFStringsaus Literalen erstellt, zu einem unerwünschten String-Konvertierungsprozess.
- Ohne das Makro
-
Vermeiden Sie, die gleiche Operation in Schleifen redundant zu wiederholen.
- Verschieben Sie gemeinsame Unterausdrücke aus Schleifen, um redundante Berechnungen zu vermeiden. Nutzen Sie Statistiken in einigen Fällen, um global redundante Operationen über Funktionsaufrufe hinweg zu vermeiden, wie z. B. die Konstruktion eines
FNameaus einem String-Literal.
- Verschieben Sie gemeinsame Unterausdrücke aus Schleifen, um redundante Berechnungen zu vermeiden. Nutzen Sie Statistiken in einigen Fällen, um global redundante Operationen über Funktionsaufrufe hinweg zu vermeiden, wie z. B. die Konstruktion eines
-
Achten Sie auf das hot reload.
- Minimieren Sie Abhängigkeiten, um die Iterationszeit zu verkürzen. Verwenden Sie kein Inlining oder Vorlagen für Funktionen, die sich wahrscheinlich über einen Reload ändern werden. Verwenden Sie „statics“ nur für Dinge, von denen erwartet wird, dass sie beim Neuladen konstant bleiben.
-
Verwenden Sie Zwischenvariablen, um komplizierte Ausdrücke zu vereinfachen.
-
Wenn Sie einen komplizierten Ausdruck haben, kann dieser leichter verständlich sein, wenn Sie ihn in Unterausdrücke aufteilen, die Zwischenvariablen zugewiesen werden, wobei die Namen die Bedeutung des Unterausdrucks innerhalb des Parent-Ausdrucks beschreiben. Zum Beispiel:
if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
Sollte ersetzt werden durch:
-
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
if (bIsLegalWindow && !bIsPlayerDead)
{
DoSomething();
}
-
Zeiger und Referenzen sollten nur ein Leerzeichen rechts vom Zeiger oder der Referenz haben.
-
Dies erleichtert die schnelle Verwendung von Find in Files für alle Zeiger oder Referenzen auf einen bestimmten Typ. Zum Beispiel:
// Verwenden Sie dies FShaderType* Ptr // Verwenden Sie dies nicht: FShaderType *Ptr FShaderType * Ptr
-
-
Schattierte Variablen sind nicht erlaubt.
-
C++ erlaubt es, dass die Variable von einem äußeren Geltungsbereich abgeschattet wird, aber dies macht die Verwendung für einen Leser mehrdeutig. Zum Beispiel gibt es drei anwendbare
Count-Variablen in dieser Member-Funktion:class FSomeClass { public: void Func(const int32 Count) { for (int32 Count = 0; Count != 10; ++Count) { // Count verwenden } } private: int32 Count; }
-
-
Vermeiden Sie die Verwendung von unsichtbaren Literalen in Funktionsaufrufen.
-
Bevorzugen Sie benannte Konstanten die ihre Bedeutung beschreiben. Dadurch wird die Absicht für einen gelegentlichen Leser offensichtlicher, da er zum Verständnis nicht die Funktionsdeklaration nachschlagen muss.
// Alter Stil Trigger(TEXT("Soldier"), 5, true);. // Neuer Stil const FName ObjectName = TEXT(„Soldat"); const float CooldownInSeconds = 5; const bool bVulnerableDuringCooldown = true; Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
-
-
Vermeiden Sie die Definition nicht-trivialer statischer Variablen in den Headern.
-
Nicht-triviale statische Variablen führen dazu, dass eine Instanz in jeder Übersetzungseinheit kompiliert wird, die diesen Header umfasst:
// SomeModule.h static const FString GUsefulNamedString = TEXT("String"); // *Ersetze das Obige mit:* // SomeModule.h extern SOMEMODULE_API const FString GUsefulNamedString; // SomeModule.cpp const FString GUsefulNamedString = TEXT("String");
-
-
Vermeide umfangreiche Änderungen, die das Verhalten des Codes nicht ändern (zum Beispiel: Ändern von Leerzeichen oder Massenumbenennen von privaten Variablen), da diese zu unnötigem Rauschen im Quellenverlauf führen und beim Zusammenführen störend sind.
-
Wenn eine solche Änderung wichtig ist, z. B. die Behebung einer fehlerhaften Einrückung, die durch ein automatisches Zusammenführungswerkzeug verursacht wurde, sollte sie allein übermittelt werden und nicht zusammen mit Verhaltensänderungen.
-
Bevorzugen Sie, Leerzeichen oder andere kleinere Verstöße der Programmierstandards nur zu beheben, wenn andere Bearbeitungen an denselben Zeilen oder in der Nähe des Codes vorgenommen werden.
-
API-Design-Richtlinien
-
Boolesche Funktionsparameter sollten vermieden werden.
-
Insbesondere sollten boolesche Parameter für Flags, die an Funktionen übergeben werden, vermieden werden. Diese haben das gleiche Problem mit anonymen Literalen wie zuvor erwähnt, aber sie neigen auch dazu, sich im Laufe der Zeit zu vermehren, wenn APIs mit mehr Verhalten erweitert werden. Bevorzugen Sie stattdessen eine Enum (weitere Informationen finden Sie im Hinweis zur Verwendung von Enums als Flag im Abschnitt Stark typisierte Enums:
// Alter Stil FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false); FCup* Cup = MakeCupOfTea(Tea, false, true, true); // Neuer Stil enum class ETeaFlags { None, Milch = 0x01, Sugar = 0x02, Honey = 0x04, Lemon = 0x08 }; ENUM_CLASS_FLAGS(ETeaFlags) FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None); FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
-
-
Diese Form verhindert das versehentliche Vertauschen von Flags, vermeidet die versehentliche Konvertierung von Zeiger- und Ganzzahlargumenten, entfernt die Notwendigkeit, redundante Standardeinstellungen zu wiederholen, und ist effizienter.
-
Es ist zulässig,
Boolsals Argumente zu verwenden, wenn sie den vollständigen Zustand darstellen, der an eine Funktion wie einen Setter übergeben werden soll, z. B.void FWidget::SetEnabled(bool bEnabled). Erwägen Sie jedoch eine Umgestaltung, wenn sich dies ändert. -
Vermeiden Sie zu lange Funktionsparameterlisten.
-
Wenn eine Funktion viele Parameter benötigt, dann erwäge, stattdessen eine eigene Struktur zu übergeben:
// Alter Stil TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f); // Neuer Stil struct FTeaPartyParams { const FTeaFlags* TeaPreferences = nullptr; uint32 NumCupsToMake = 0; FKettle* Kettle = nullptr; ETeaType TeaType = ETeaType::EnglishBreakfast; float BrewingTimeInSeconds = 120.0f; }; TUniquePtr<FCup[]> MakeTeaForParty(const FTeaPartyParams& Params);
-
-
Vermeiden Sie Überladungsfunktionen durch
boolundFString.-
Dies kann zu unerwartetem Verhalten führen:
void Func(const FString& String); void Func(bool bBool); Func(TEXT("String")); // Ruft die bool-Überladung auf!
-
-
Interface-Klassen sollten immer abstrakt sein.
- Interface-Klassen haben das Präfix „I“ und müssen keine Member-Variablen besitzen. Interfaces dürfen Methoden enthalten, die nicht rein virtuell sind, und können Methoden enthalten, die nicht virtuell oder statisch sind, solange sie inline implementiert werden.
-
Verwenden Sie die Schlüsselwörter
virtualundoverride, wenn Sie eine überschreibende Methode deklarieren.
Wenn Sie in einer abgeleiteten Klasse eine virtuelle Funktion deklarieren, die eine virtuelle Funktion in der Parent-Klasse überschreibt, müssen Sie sowohl das Schlüsselwort virtual als auch das Schlüsselwort override verwenden. Zum Beispiel:
class A
{
public:
virtual void F() {}
};
class B : public A
{
public:
virtual void F() override;
}
Aufgrund der kürzlichen Hinzufügung des Schlüsselworts override befolgt zahlreicher vorhandener Code dies noch nicht. Das Schlüsselwort override sollte zu diesem Code hinzugefügt werden, wenn es möglich ist.
-
UObjects sollten mittels Zeiger übergeben werden, nicht als Referenz. Wenn eine Funktion nicht „null“ erwartet, sollte dies von der API dokumentiert oder entsprechend behandelt werden. Zum Beispiel:
// Schlecht void AddActorToList(AActor& Obj); // Gut void AddActorToList(AActor* Obj);
Plattformspezifischer Code
Plattformspezifischer Code sollte immer abstrahiert und in plattformspezifischen Quelldateien in entsprechend benannten Unterverzeichnissen implementiert werden, zum Beispiel:
Engine/Platforms/[PLATFORM]/Source/Runtime/Core/Private/[PLATFORM]PlatformMemory.cpp
Im Allgemeinen sollten Sie das Hinzufügen jeglicher Verwendung von PLATFORM_[PLATFORM] vermeiden. Vermeiden Sie beispielsweise das Hinzufügen von PLATFORM_XBOXONE zu Code außerhalb eines Verzeichnisses mit dem Namen [PLATFORM]. Erweitern Sie stattdessen die Hardware-Abstraktionsebene, um eine statische Funktion hinzuzufügen, z. B. in FPlatformMisc:
FORCEINLINE static int32 GetMaxPathLength()
{
return 128;
}
Plattformen können dann diese Funktion überschreiben, indem sie entweder einen plattformspezifischen konstanten Wert zurückgeben oder sogar die Plattform-API verwenden, um das Ergebnis zu bestimmen. Wenn Sie die Funktion zwangsweise einbinden, weist sie dieselben Performance-Merkmale auf wie bei Verwendung einer Definition.
In Fällen, in denen eine Definition unbedingt erforderlich ist, erstellen Sie neue #define-Direktiven, die bestimmte Eigenschaften beschreiben, die auf eine Plattform angewendet werden können, z. B. PLATFORM_USE_PTHREADS. Legen Sie den Standardwert in Platform.h fest und überschreiben Sie ihn für alle Plattformen, die ihn benötigen, in der plattformspezifischen Datei Platform.h.
Zum Beispiel haben wir in Plattform.h:
#ifndef PLATFORM_USE_PTHREADS
#define PLATFORM_USE_PTHREADS 1
#endif
WindowsPlatform.h hat:
#define PLATFORM_USE_PTHREADS 0
Plattformübergreifender Code kann die Definition dann direkt verwenden, ohne die Plattform kennen zu müssen.
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
Wir zentralisieren die plattformspezifischen Details der Engine, sodass diese vollständig in plattformspezifischen Quelldateien enthalten sein können. Auf diese Weise wird es einfacher, die Engine über mehrere Plattformen hinweg zu pflegen. Außerdem können Sie den Code auf neue Plattformen portieren, ohne die Codebasis nach plattformspezifischen Definitionen durchsuchen zu müssen.
Das Aufbewahren von Plattform-Code in plattformspezifischen Ordnern ist auch eine Anforderung für NDA-Plattformen wie PlayStation, Xbox und Nintendo Switch.
Es ist wichtig, darauf zu achten, dass der Code kompiliert wird und unabhängig davon ausgeführt wird, ob das Unterverzeichnis [PLATFORM] vorhanden ist. Mit anderen Worten, plattformübergreifender Code sollte niemals von plattformspezifischem Code abhängig sein.