Epic Games a mis en place des normes et conventions de codage simples. Ce document présente l'état des normes de codage actuelles d'Epic Games. Le respect des normes de codage est obligatoire.
Les conventions de code sont importantes pour les programmeurs pour plusieurs raisons :
-
80 % du coût d'un logiciel est consacré à la maintenance.
-
Pratiquement aucun logiciel n'est géré par l'auteur original tout au long de sa durée de vie.
-
Les conventions de codage améliorent la lisibilité du logiciel, ce qui permet aux techniciens de comprendre le nouveau code rapidement dans son intégralité.
-
Si nous décidons d'exposer le code source aux développeurs de la communauté de modérateurs, il doit être facilement compréhensible.
-
Le respect de la plupart de ces conventions est nécessaire pour assurer la compatibilité entre compilateurs.
Bien que les normes de codage ci-dessous s'appliquent à C++, vous devez les suivre, quel que soit le langage que vous utilisez. Le cas échéant, une section peut spécifier des règles ou des exceptions équivalentes pour des langages spécifiques.
Organisation des classes
Vous devez organiser les classes en ayant à l'esprit le lecteur, et non le rédacteur. Étant donné que la plupart des lecteurs utiliseront l'interface publique de la classe, l'implémentation publique des classes doit être déclarée en premier, suivie de l'implémentation privée.
UCLASS()
class EXAMPLEPROJECT_API AExampleActor : public AActor
{
GENERATED_BODY()
public:
// Définit les valeurs par défaut des propriétés de cet acteur
AExampleActor();
protected:
// Appelé au démarrage du jeu ou lors de l'apparition
virtual void BeginPlay() override;
};
Avis de copyright
Tout fichier source (.h, .cpp, .xaml) fourni par Epic Games en vue d'être distribué publiquement doit inclure un avis de copyright dans la première ligne du fichier. Le format de cet avis doit correspondre exactement à celui indiqué ci-dessous :
// Copyright Epic Games, Inc. Tous droits réservés.
Si cette ligne est manquante ou n'est pas correctement formatée, CIS génère une erreur et échoue.
Conventions d'affectation de noms
Lors de l'utilisation des conventions d'affectation de noms, le code et les commentaires doivent utiliser l'orthographe et la grammaire anglaise des États-Unis.
- La première lettre de chaque mot dans un nom (tel que le nom du type ou le nom de la variable) est en majuscule. Il n'y a généralement pas de trait de soulignement entre les mots. Par exemple,
HealthetUPrimitiveComponentsont corrects, mais "lastMouseCoordinates" et "delta_coordinates" ne le sont pas.
Il s'agit d'un formatage PascalCase pour les utilisateurs qui peuvent connaître d'autres langages de programmation orientés objet.
-
Les noms de type sont précédés d'une lettre majuscule supplémentaire pour les distinguer des noms de variables. Par exemple,
FSkinest un nom de type etSkinest une instance du typeFSkin. -
Les modèles de classe sont précédés du préfixe T.
modèle <typename ObjectType> class TAttribute -
Les classes qui héritent d'UObject sont précédées du préfixe U.
class UActorComponent -
Les classes qui héritent d'AActor sont précédées du préfixe A.
class AActor -
Les classes qui héritent de SWidget sont précédées du préfixe S.
class SCompoundWidget -
Les classes qui sont des interfaces abstraites sont précédées du préfixe I.
class IAnalyticsProvider -
Les types de structure semblables à des concepts d'Epic sont précédés du préfixe C.
struct CStaticClassProvider { template <typename T> auto Requires(UClass*& ClassRef) -> decltype( ClassRef = T::StaticClass() ); }; -
Les énumérations sont préfixées par E.
enum class EColorBits { BCE_Rouge, BCE_Vert, BCE_Bleu }; -
Les variables booléennes doivent être précédées du préfixe b.
bPendingDestruction bHasFadedIn -
La plupart des autres classes sont précédées du préfixe F, bien que certains sous-systèmes utilisent d'autres lettres.
-
Les définitions de type doivent être précédées d'un préfixe approprié à ce type, par exemple :
-
F, pour la définition de type d'une structure
-
U, pour la définition de type d'un
UObject
-
-
Une définition de type d'une instanciation de modèle particulière n'est plus un modèle et doit être précédée du préfixe approprié.
typedef TArray<FMytype> FArrayOfMyTypes; -
Les préfixes sont omis dans C#.
-
L'outil Unreal Header nécessite les bons préfixes dans la plupart des cas, il est donc important de les spécifier.
-
Les paramètres de modèle de type et les alias de type imbriqués basés sur ces paramètres de modèle ne sont pas soumis aux règles de préfixe ci-dessus, car la catégorie de type est inconnue.
-
Préférez un suffixe de type après un terme descriptif.
-
Levez l'ambiguïté entre les paramètres de modèle et les alias en utilisant un préfixe In :
template <typename InElementType> class TContainer { public: en utilisant ElementType = InElementType; }; -
Les noms de type et de variable sont des noms.
-
Les noms de méthode sont des verbes qui décrivent l'effet de la méthode ou la valeur de retour d'une méthode sans effet.
-
Les noms de macro doivent être entièrement en majuscules, séparés par des traits de soulignement et précédés du préfixe
UE_.#define UE_AUDIT_SPRITER_IMPORT
Les noms de variable, de méthode et de classe doivent être :
-
clairs
-
sans ambiguïté
-
descriptifs
Plus l'étendue du nom est importante, plus le nom doit être descriptif et précis. Évitez le recours excessif aux abréviations.
Toutes les variables doivent être déclarées sur leur propre ligne afin que vous puissiez fournir des commentaires sur la signification de chaque variable.
Le style JavaDocs l'exige.
Vous pouvez formuler des commentaires sur plusieurs lignes ou sur une seule ligne avant une variable. Les lignes vierges sont facultatives pour regrouper les variables.
Toutes les fonctions qui renvoient une valeur booléenne doivent poser une question true/false, telle que IsVisible() ou ShouldClearBuffer().
Une procédure (une fonction sans valeur de retour) doit utiliser un verbe fort suivi d'un objet. Une exception a lieu si l'objet de la méthode est l'objet dans lequel elle se trouve. Dans ce cas, le contexte permet de comprendre l'objet. Les noms à éviter sont ceux commençant par "Handle" et "Process", car les verbes sont ambigus.
Nous vous encourageons à faire précéder les noms des paramètres de fonction du préfixe "Out" si :
-
les paramètres de fonction sont transmis par référence.
-
la fonction est censée s'écrire sur cette valeur.
Il est ainsi évident que la valeur transmise dans cet argument est remplacée par la fonction.
Si un paramètre In ou Out est également un booléen, insérez "b" avant le préfixe In/Out, par exemple bOutResult.
Les fonctions qui renvoient une valeur doivent décrire la valeur de retour. Le nom doit indiquer clairement la valeur que renvoie la fonction. Cela est particulièrement important pour les fonctions booléennes. Prenons les deux exemples de méthodes suivants :
// que signifie vrai ?
bool CheckTea(FTea Tea);
// le nom indique clairement que true signifie que le thé est frais
bool IsTeaFresh(FTea Thé);
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
Choix de mots inclusifs
Lorsque vous travaillez sur le code de l'Unreal Engine, nous vous encourageons à utiliser un langage respectueux, inclusif et professionnel.
Choisissez les mots avec prudence lorsque vous nommez :
-
des classes,
-
des fonctions,
-
des structures de données,
-
des types,
-
des variables,
-
des fichiers et des dossiers,
-
des plug-ins.
Cette règle s'applique lorsque vous rédigez des extraits de texte destinés à l'utilisateur dans l'interface utilisateur, les messages d'erreur et les notifications. Elle s'applique également lors de l'écriture de code, notamment dans les commentaires et les descriptions de listes de modifications.
Consultez la section suivante afin d'obtenir des conseils et des suggestions pour choisir des termes et des noms respectueux et appropriés à toutes les situations et à tous les publics, de façon à communiquer plus efficacement.
Inclusion raciale, ethnique et religieuse
-
N’utilisez pas de métaphores ou de comparaisons qui renforcent les stéréotypes. Les exemples incluent le contraste noir et blanc ou la liste noire et la liste blanche.
-
N'utilisez pas de termes faisant référence à un traumatisme historique ou à des expériences de discrimination. Par exemple : esclave, maître et bombe.
Inclusion des genres
-
Évitez d'utiliser des pronoms sexués si vous vous référez à des personnes hypothétiques. Utilisez they, them en anglais, même au singulier.
-
Désignez les objets avec l'article approprié (it et its. en anglais). Par exemple, un module, un plug-in, une fonction, un client, un serveur ou tout autre composant logiciel ou matériel.
-
N'attribuez pas de genre à ce qui n'en a pas.
-
N'utilisez pas de noms collectifs comme tous (dans l'expression "bonjour à tous") (hey guys), qui supposent un genre.
-
Évitez les expressions familières qui contiennent des genres arbitraires, comme "femme au foyer" (par exemple, poor man's X en anglais).
Argot
-
N'oubliez pas que les mots que vous employez sont lus par un vaste public qui ne partage peut-être pas les mêmes expressions et attitudes que vous, et qui ne comprend peut-être pas les mêmes références culturelles.
-
Évitez les termes argotiques et les expressions familières, même si vous pensez qu'ils sont drôles ou inoffensifs. Ils peuvent être difficiles à comprendre pour les personnes dont la langue maternelle n'est pas la vôtre et s'avérer difficilement traduisibles.
-
N'utilisez pas de termes grossiers ou obscènes.
Mots surchargés
- De nombreux termes que nous utilisons habituellement pour leurs significations techniques ont également d'autres significations dans d'autres registres. Par exemple, abandonner, exécuter_ ou natif (abort, execute, or native., en anglais). Lorsque vous utilisez ce type de mots, soyez toujours précis et examinez le contexte dans lequel ils apparaissent.
Liste de mots
La liste suivante identifie certains termes que nous avons utilisés dans la base de code Unreal dans le passé, mais qui, selon nous, doivent être remplacés par des termes plus adaptés :
| Mot employé | Mot alternatif |
|---|---|
| Liste noire | _liste de refus_, _liste de blocage_, _liste d'exclusion_, _liste d'évitement_, _liste non approuvée_, _liste interdite_,liste d'autorisation_ |
| Liste blanche | liste autorisée_, liste d'inclusion, liste de confiance, liste sûre, liste préférée, liste approuvée, liste d'autorisation_ |
| Maître | principal, source, contrôleur, modèle_, référence_, leader, original, élémentaire |
| Esclave | secondaire, réplique, agent, _abonné, travailleur, nœud de cluster_, verrouillé, lié, synchronisé |
Nous travaillons activement à mettre notre code en conformité avec les principes énoncés ci-dessus.
Code C++ portable
La taille du type int et des types int non signés varie selon la plateforme. La largeur de ces types doit être d'au moins 32 bits. Vous pouvez utiliser ces types dans le code si la largeur du nombre entier n'a pas d'importance. Les types de taille explicite sont utilisés dans les formats sérialisés ou répliqués.
Voici une liste des types les plus courants :
-
boolpour les valeurs booléennes (veillez à NE JAMAIS supposer la taille du typebool).BOOLne sera pas compilé. -
TCHARpour un personnage (veillez à NE JAMAIS supposer la taille du typeTCHAR). -
uint8pour les octets non signés (1 octet). -
int8pour les octets signés (1 octet). -
uint16pour les entiers courts non signés (2 octets). -
int16pour les entiers courts signés (2 octets). -
uint32pour les entiers non signés (4 octets). -
int32pour les entiers signés (4 octets). -
uint64` pour les mots quad non signés (8 octets). -
uint64` pour les mots quad signés (8 octets). -
floatpour un nombre à virgule flottante simple précision (4 octets). -
doublepour un nombre à virgule flottante double précision (8 octets). -
PTRINTpour un nombre entier pouvant contenir un pointeur (veillez à ne JAMAIS supposer la taille dePTRINT).
Utiliser les bibliothèques standard
Historiquement, l'UE a évité l'utilisation directe des bibliothèques standard C et C++ pour les raisons suivantes :
-
Remplacer les implémentations lentes par les nôtres, qui fournissent des contrôles supplémentaires sur l'allocation de mémoire.
-
Ajouter de nouvelles fonctionnalités avant qu'elles ne soient largement disponibles, telles que :
-
Appliquer des modifications de comportement souhaitables, mais non conformes.
-
Disposer d'une syntaxe cohérente dans toute la base de code.
-
Éviter les constructions incompatibles avec les idiomes de l'UE.
-
Cependant, la bibliothèque standard a évolué et inclut des fonctionnalités que nous ne souhaitons pas rendre abstraites ou réimplémenter nous-mêmes.
Lorsque vous avez le choix entre une fonctionnalité de bibliothèque standard et celle que nous vous offrons, vous devez porter votre choix sur l'option qui fournit le meilleur résultat. Il est également important de rappeler que la cohérence est de mise. Si une ancienne implémentation de l'UE ne sert plus à rien, nous pouvons décider de ne plus l'utiliser et de migrer toute utilisation vers la bibliothèque standard.
Évitez de mélanger les idiomes de l'UE et ceux de la bibliothèque standard dans la même API. Consultez le tableau suivant pour connaître les idiomes courants et savoir quand les utiliser.
| Idiome | Description |
|---|---|
<atomic> |
L'idiome atomique doit être utilisé dans le nouveau code. L'ancien code doit être migré lorsqu'il est modifié. Vous êtes censé mettre en œuvre les atomiques de manière complète et efficace sur toutes les plateformes prises en charge. Notre propre idiome TAtomic n'est que partiellement implémenté, et il n'est pas dans notre intérêt de le maintenir et de l'améliorer. |
<type_traits> |
L'idiome de traits de type doit être utilisé en cas de chevauchement entre un ancien trait de l'UE et un trait standard. Les traits sont souvent implémentés en tant qu'éléments intrinsèques du compilateur à des fins de correction, et le compilateur peut avoir connaissance des traits standard et sélectionner des chemins de compilation plus rapides au lieu de les traiter comme du C++ brut. Problème : nos traits ont généralement une définition de type statique Value ou Type en majuscules, alors que les traits standard sont censés utiliser value et type. Il s'agit d'une distinction importante, car une syntaxe particulière est attendue par les traits compositionnels, par exemple std::conjunction. Les nouveaux traits que nous ajoutons doivent être écrits en minuscules, à savoir value ou type pour prendre en charge la composition. Les traits existants doivent être mis à jour pour prendre en charge l'un ou l'autre cas. |
<initializer_list> |
L'idiome de liste de l'initialiseur doit être utilisé pour prendre en charge la syntaxe de l'initialiseur entre accolades. Il est utilisé en cas de chevauchement entre le langage et la bibliothèque standard. Il n'existe aucune autre alternative si vous souhaitez le prendre en charge. |
<regex> |
L'idiome d'expression régulière peut être utilisé directement, mais son utilisation doit être encapsulée dans du code réservé à l'éditeur. Nous n'avons pas l'intention d'implémenter notre propre solution d'expression régulière. |
<limits> |
std::numeric_limits peut être utilisé dans son intégralité. |
<cmath> |
Toutes les fonctions à virgule flottante de cet en-tête peuvent être utilisées. |
<cstring>: memcpy() et memset() |
Ces idiomes peuvent être utilisés à la place de FMemory::Memcpy et FMemory::Memset respectivement, lorsqu'ils présentent un avantage démontrable en matière de performances. |
Les conteneurs et chaînes standard doivent être évités, sauf dans le code d'interopérabilité.
Commentaires
Les commentaires permettent de communiquer, et la communication est vitale. Les sections suivantes présentent certains points importants dont vous devez tenir compte lors de l'insertion de commentaires (Kernighan & Pike The Practice of programming).
Instructions
-
Rédigez du code autodocumenté. Par exemple :
// Mauvais: t = s + l - b; // Correct : TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Insérez des commentaires utiles. Par exemple :
// Mauvais: // incrémenter les feuilles ++Feuilles; // Correct : // nous savons qu'il existe une autre feuille de thé ++Feuilles; -
Évitez de trop commenter le mauvais code : optez plutôt pour le réécrire. Par exemple :
// Mauvais: // le nombre total de feuilles est la somme de // petites et grandes feuilles moins les // nombre de feuilles qui sont à la fois t = s + l - b; // Correct : TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; -
Ne contredisez pas le code. Par exemple :
// Mauvais: // ne jamais incrémenter les feuilles ! ++Feuilles; // Correct : // nous savons qu'il existe une autre feuille de thé ++Feuilles;
Const Correctness
Le mot-clé const sert autant de documentation que d'instruction de compilation. Dans la mesure du possible, le code doit répondre à la const-correctness. Vous devez donc respecter les consignes suivantes :
-
Transmettre les arguments de fonction par un pointeur const ou une référence si ces arguments ne doivent pas être modifiés par la fonction.
-
Signaler les méthodes avec le mot-clé const si elles ne modifient pas l'objet.
-
Utiliser l'itération const sur le conteneur si la boucle ne doit pas modifier le conteneur.
Exemple de const :
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray ne sera pas modifié ici, mais OutResult le sera probablement
}
void FThing::SomeNonMutatingOperation() const
{
// Ce code ne modifiera pas le FThing sur lequel il est invoqué
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// Le corps de cette boucle ne modifiera pas StringArray
}
Préférez également const pour les paramètres de fonction et les paramètres locaux par valeur. Il indique au lecteur que la variable ne sera pas modifiée dans le corps de la fonction, ce qui en facilite la compréhension. Le cas échéant, assurez-vous que la déclaration et la définition correspondent, car cela peut avoir une incidence sur le processus JavaDoc.
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Ni Count ni CountPlusOne ne peuvent être modifiés dans le corps de la fonction
}
Une exception à cette règle concerne les paramètres transmis par valeur, qui sont déplacés dans un conteneur. Pour en savoir plus, consultez la rubrique "Sémantique du déplacement" de cette page.
Exemple :
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
Placez le mot-clé const à la fin lorsque vous rendez un pointeur constant (plutôt que ce vers quoi il pointe). Il n'est quoi qu'il en soit pas possible de "réaffecter" les références et donc de les rendre constantes.
Exemple :
// Pointeur constant vers objet non constant - il est impossible de réaffecter le pointeur, mais il est toujours possible de modifier T
T* const Ptr = ...;
// Illégal
T& const Ref = ...;
N'utilisez jamais const sur un type de retour. Cela inhibe la sémantique de déplacement pour les types complexes et génère des avertissements de compilation pour les types intégrés. Cette règle s'applique uniquement au type de retour proprement dit, et non au type cible d'une référence ou d'un pointeur renvoyé.
Exemple :
// Incorrect - renvoie d'une matrice constante
const TArray<FString> GetSomeArray();
// Correct - renvoie d'une référence à une matrice constante
const TArray<FString>& GetSomeArray();
// Correct - renvoie d'un pointeur à une matrice constante
const TArray<FString>* GetSomeArray();
// Incorrect - renvoie d'un pointeur constant à une matrice constante
const TArray<FString>* const GetSomeArray();
Exemple de formatage
Dans la mesure où nous utilisons un système basé sur JavaDoc pour extraire les commentaires du code et créer la documentation automatiquement, nous recommandons d'utiliser des règles de formatage de commentaires spécifiques.
L'exemple suivant illustre le format des commentaires de classe, méthode et variable. N'oubliez pas que les commentaires doivent accompagner et compléter le code. Le code explique la mise en œuvre, tandis que les commentaires expliquent l'intention. Assurez-vous de mettre à jour les commentaires lorsque vous modifiez l'intention d'un extrait de code.
Notez que deux styles de commentaires de paramètres différents, illustrés par les méthodes Steep et Sweeten, sont pris en charge. Le style @param utilisé par Steep est le style multiligne traditionnel. Dans le cas d'une fonction simple, il peut s'avérer plus clair d'intégrer la documentation des paramètres et des valeurs de retour dans les commentaires descriptifs de la fonction. Consultez l'exemple Sweeten pour en savoir plus. Les balises de commentaire spéciales telles que @see ou @return ne doivent être utilisées que pour démarrer de nouvelles lignes suivant la description principale.
Les commentaires de méthode ne doivent être inclus qu'une seule fois, c'est-à-dire lorsque la méthode est déclarée publiquement. Les commentaires de méthode ne doivent contenir que des informations pertinentes destinées aux appelants de la méthode, notamment toute information sur les remplacements de la méthode susceptibles d'être utiles à l'appelant. Les détails concernant la mise en œuvre de la méthode et ses remplacements, qui ne concernent pas l'appelant, doivent être commentés dans l'implémentation de la méthode.
Les commentaires de classe doivent inclure :
-
Une description du problème que cette classe résout.
-
La raison pour laquelle cette classe a été créée.
Les commentaires relatifs à une méthode multiligne doivent inclure :
-
Un objectif de fonction : qui documente le problème que cette fonction résout. Comme indiqué précédemment, les commentaires expliquent l'intention, et le code explique l'implémentation.
-
Commentaires de paramètre : chaque commentaire de paramètre doit comprendre :
-
unités de mesure;
-
la plage de valeurs attendues ;
-
des valeurs "impossibles" ;
-
la signification des codes de statut/d'erreur.
-
-
**Commentaires de retour : ces commentaires documentent la valeur de retour attendue, de la même façon qu'une variable de sortie est documentée. Pour éviter la redondance, évitez d'utiliser un commentaire explicite
@returnsi le seul but de la fonction est de renvoyer cette valeur et que cela est déjà documenté dans l'objectif de la fonction. -
Informations supplémentaires : il est éventuellement possible d'utiliser
@warning,@note,@seeet@deprecatedpour documenter des informations supplémentaires pertinentes. Chacune de ces informations doit être déclarée sur sa propre ligne après les autres commentaires.
Syntaxe du langage C++ moderne
L'Unreal Engine ayant été conçu dans un souci de portabilité pour de nombreux compilateurs C++, nous veillons à utiliser des fonctionnalités compatibles avec les compilateurs que nous prenons en charge. Les fonctionnalités sont parfois tellement utiles que nous les encapsulons dans des macros et les utilisons de manière généralisée. Néanmoins, nous attendons généralement que tous les compilateurs que nous prenons en charge répondent aux dernières normes.
L'Unreal Engine est compilé avec une version de langage de C++20 par défaut et nécessite une version minimale de C++20. Nous utilisons de nombreuses fonctionnalités de langage modernes qui sont prises en charge par les compilateurs actuels. Dans certains cas, nous enveloppons l'utilisation de ces fonctionnalités dans des conditionnelles de préprocesseur. Cependant, nous décidons parfois d'éviter complètement certaines fonctionnalités du langage pour des raisons de portabilité ou autres.
Sauf indication contraire ci-dessous, en tant que fonctionnalité de compilateur C++ moderne que nous prenons en charge, vous ne devez pas utiliser les fonctionnalités de langage propres au compilateur, à moins qu'elles soient encapsulées dans des macros ou des conditionnelles de préprocesseur et utilisées avec parcimonie.
Assertion statique
Le mot-clé static_assert est valable lorsque vous souhaitez disposer d'une assertion lors de la compilation.
Mots-clés override et final
L'emploi des mots-clés override et final est vivement encouragé. Il est possible que ces mots-clés aient été omis à de nombreux emplacements du code. Ce problème sera corrigé petit à petit.
nullptr
Vous devez utiliser nullptr au lieu de la macro NULL de style C dans tous les cas.
Une exception à cette règle est l'utilisation de nullptr dans les versions C++/CX (p. ex., dans Xbox One). Dans ce cas, l'utilisation de nullptr correspond en réalité au type de référence null géré. Il est principalement compatible avec nullptr du langage C++ natif, sauf dans son propre type et dans certains contextes d'instanciation de modèle. Vous devez donc utiliser la macro TYPE_OF_NULLPTR au lieu de decltype(nullptr) à des fins de compatibilité.
auto
Vous ne devez pas utiliser auto dans le code C++, sauf dans les quelques cas répertoriés ci-dessous. Soyez toujours explicite concernant le type que vous initialisez. Autrement dit, le type doit être clairement visible pour le lecteur. Cette règle s'applique également à l'utilisation du mot-clé var dans C#.
Veillez également à ne pas utiliser la fonctionnalité de liaison structurée de C++20, car il s'agit en fait d'un mot-clé auto variadique.
Quand utiliser auto ?
-
Lorsque vous devez lier un lambda à une variable, car il n'est pas possible d'exprimer les types lambda dans le code.
-
Pour les variables d'itérateur, mais uniquement lorsque le type de l'itérateur est détaillé et nuirait à la lisibilité.
-
Dans le code du modèle, lorsqu'il est difficile de discerner le type d'une expression. Il s'agit d'un cas avancé.
Il est capital que les types soient clairement visibles à toute personne qui lit le code. Même si certains EDI sont capables de déduire le type, le code doit pour cela se trouver dans un état compilable. Le mot-clé auto n'est pas non plus utile aux utilisateurs d'outils de fusion/comparaison ni les personnes qui consultent des fichiers sources individuels isolés, comme sur GitHub.
Si vous êtes sûr d'utiliser auto de manière acceptable, n'oubliez jamais d'utiliser correctement const, & ou *, comme vous le feriez avec le nom du type. Avec l'utilisation du mot-clé auto, vous manipulez le type déduit à votre guise.
for basé sur la plage
Préférez cet itérateur pour faciliter la compréhension et la gestion du code. Lorsque vous migrez du code qui utilise les anciens itérateurs TMap, gardez à l'esprit que les anciennes fonctions Key() et Value(), qui étaient des méthodes du type d'itérateur, sont désormais simplement des champs Key et Value de la TPair clé-valeur sous-jacente.
Exemple :
TMap<FString, int32> MyMap;
// Ancien style
pour (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// Nouveau style
pour (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
Il existe aussi des remplacements de plage pour certains types d'itérateurs autonomes.
Exemple :
// Ancien style
pour (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// Nouveau style
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
Fonctions lambda et anonymes
Vous pouvez utiliser les lambda librement, bien qu'ils présentent des problèmes de sécurité supplémentaires. Les lambda les mieux appropriés ne doivent comporter que quelques instructions, en particulier lorsqu'ils sont utilisés dans le cadre d'une expression ou d'une instruction plus longue, par exemple en tant que prédicat dans un algorithme générique.
Exemple :
// Rechercher le premier élément dont le nom contient le mot "Hello"
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });
// Trier la matrice dans l'ordre inverse du nom
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });|
Gardez à l'esprit que les lambda avec état ne peuvent pas être affectés à des pointeurs de fonction, que nous avons tendance à beaucoup utiliser. Les lambda non triviaux doivent être documentés de la même manière que les fonctions standard. Il est par ailleurs possible d'utiliser les lambda comme délégués pour une exécution différée en utilisant des fonctions telles que BindWeakLambda, où les variables capturées fonctionnent comme une charge utile.
Captures et types de retour
Il est conseillé d'utiliser des captures explicites plutôt que des captures automatiques ([&] et [=]). Cela est important pour des raisons de lisibilité, de maintenabilité, de sécurité et de performances, en particulier lors de leur utilisation avec des lambda volumineux et une exécution différée.
Les captures explicites déclarent l'intention de l'auteur. Par conséquent, les erreurs sont détectées lors de l'examen du code. Des captures incorrectes peuvent provoquer de graves bogues et des blocages qui peuvent s'avérer problématiques, car le code est géré au fil du temps. Tenez compte des éléments supplémentaires suivants concernant les captures lambda :
-
La capture par référence et la capture par valeur des pointeurs (y compris le pointeur
this) peuvent endommager les données et provoquer des blocages si l'exécution du lambda est différée. Les variables locales et membres ne doivent jamais être capturées par référence pour les lambda différés. -
La capture par valeur peut entraîner des problèmes de performances si elle crée des copies inutiles pour un lambda non différé.
-
Les pointeurs UObject capturés accidentellement sont invisibles lors du nettoyage de mémoire. La capture automatique intercepte
thisde manière implicite si une variable membre est référencée, même si[=]donne l'impression que le lambda a ses propres copies de tout. -
Les wrappers délégués tels que
CreateWeakLambdaetCreateSPLambdadoivent être utilisés pour l'exécution différée, car ils se dissocient automatiquement si l'UObject ou le pointeur partagé sont libérés. Il est possible de capturer d'autres objets partagés tels que TWeakObjectPtr ou TWeakPtr, puis de les valider à l'intérieur du lambda. -
Toute utilisation différée du lambda qui ne respecte pas ces directives doit être accompagnée d'un commentaire expliquant les raisons pour lesquelles la capture lambda est sûre.
Les types de retour explicites doivent être utilisés pour les lambda volumineux, ou lorsque vous renvoyez le résultat d'un autre appel de fonction. Ceux-ci doivent être considérés de la même manière que le mot-clé auto.
Classes Enum fortement typées
Les classes énumérées (Enum) remplacent les énumérations à espace de noms de l'ancien style, à la fois pour les énumérations régulières et les UENUMs Par exemple :
// Ancienne enum
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// Nouvelle enum
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
Les classes énumérées sont prises en charge en tant que UPROPERTYs et remplacent l'ancienne solution de contournement TEnumAsByte<>. Les propriétés Enum peuvent également être de toute taille, et pas seulement en octets :
// Ancienne propriété
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// Nouvelle propriété
UPROPERTY()
EThing MyProperty;
Les enums exposées aux blueprints doivent continuer à être basées sur uint8.
Les classes Enum utilisées comme indicateurs peuvent faire appel à la macro ENUM_CLASS_FLAGS(EnumType) pour définir automatiquement tous les opérateurs de bits :
enum class EFlags
{
Aucun = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
La seule exception à cette règle est l'utilisation d'indicateurs dans un contexte de vérité. Il s'agit d'une limitation du langage. Au lieu de cela, tous les indicateurs d'énumération doivent avoir un énumérateur appelé None, défini sur 0 à des fins de comparaison :
// Ancien
if (Flags & EFlags::Flag1)
// Nouveau
if ((Flags & EFlags::Flag1) != EFlags::None)
Sémantique du déplacement
Tous les principaux types de conteneurs, à savoir TArray, TMap, TSet, FString, disposent de constructeurs de déplacement et d'opérateurs d'affectation de déplacement. Ceux-ci sont souvent utilisés automatiquement lors de la transmission ou du renvoi de ces types par valeur. Il est également possible de les invoquer de manière explicite à l'aide de MoveTemp, l'équivalent de std::move dans l'UE.
Le renvoi de conteneurs ou de chaînes par valeur peut être avantageux en matière d'expressivité, tout en évitant le coût habituel des copies temporaires. Les règles concernant la transmission par valeur et l'utilisation de MoveTemp sont en cours de développement, mais se trouvent dans certaines zones optimisées de la base de code.
Initialiseurs de membre par défaut
Il est possible d'utiliser les initialiseurs de membre par défaut pour définir les valeurs par défaut d'une classe à l'intérieur de la classe proprement dite :
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;
};
Le code écrit comme ceci présente les avantages suivants :
-
Il n'est pas nécessaire de dupliquer les initialiseurs sur plusieurs constructeurs.
-
Il n'est pas possible de mélanger l'ordre d'initialisation et l'ordre de déclaration.
-
Le type de membre, les indicateurs de propriété et les valeurs par défaut sont spécifiés au même emplacement. Ce style facilite la lisibilité et la gestion.
Il présente néanmoins certains inconvénients :
-
Toute modification des valeurs par défaut nécessite une reconstruction de tous les fichiers dépendants.
-
Il est impossible de modifier les en-têtes dans les versions de correctif du moteur. Ce style peut donc limiter les types de correctifs possibles.
-
Certains éléments ne peuvent pas être initialisés de cette façon, notamment les classes de base, les sous-objets
UObject, les pointeurs vers les types déclarés directement, les valeurs déduites des arguments du constructeur et les membres initialisés sur plusieurs étapes. -
L'insertion de certains initialiseurs dans l'en-tête et des initialiseurs restants dans les constructeurs au sein du fichier .cpp peut réduire la lisibilité et entraver la gestion.
Faites preuve de bon sens lorsque vous décidez d'utiliser les initialiseurs de membre par défaut. En règle générale, les initialiseurs de membre par défaut sont plus utiles dans le code du jeu que dans celui du moteur. Pensez à utiliser des fichiers de configuration pour les valeurs par défaut.
Code tiers
Chaque fois que vous modifiez le code d'une bibliothèque utilisée dans le moteur, veillez à baliser vos modifications avec le commentaire //@UE5 et à expliquer les raisons de cette modification. Cela facilite la fusion des modifications dans une nouvelle version de cette bibliothèque. Par ailleurs, les titulaires de licence peuvent facilement identifier toutes les modifications effectuées.
Tout code tiers inclus dans le moteur doit être accompagné de commentaires formatés afin de pouvoir l'identifier rapidement. Par exemple :
// @code tiers - BEGIN PhysX
#include <physx.h>
// @code tiers - FIN PhysX
// @code tiers - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
// Utilisé pour définir le nom du thread dans le débogueur
...
//@code tiers - END MSDN SetThreadName
Mise en forme du code
Accolades
L'utilisation d'accolades peut s'avérer problématique. Epic Games a pris depuis longtemps l'habitude de placer les accolades sur une nouvelle ligne. Nous vous prions de respecter cet usage, quelle que soit la taille de la fonction ou du bloc. Par exemple :
// Mauvais
int32 GetSize() const { return Size; }
// Correct
int32 GetSize() const
{
return Size;
}
Veillez à toujours inclure les accolades dans les blocs à instruction unique. Par exemple :
if (bThing)
{
return;
}
Si - Sinon
Chaque bloc d'exécution dans une instruction if-else doit être placé entre accolades. Cela permet d'éviter les erreurs de modification. Si les accolades ne sont pas utilisées, une personne est susceptible d'ajouter involontairement une ligne supplémentaire à un bloc if. Dans ce cas, cette ligne supplémentaire ne serait pas contrôlée par l'expression if, ce qui serait problématique. Cela serait également problématique si les éléments compilés de manière conditionnelle provoquaient la rupture des instructions if/else. Par conséquent, veillez à toujours utiliser des accolades.
si (bHaveUnrealLicense)
{
InsertYourGameHere();
}
autre
{
CallMarkRein();
}
Pour mettre en retrait une instruction if multidirectionnelle, vous devez mettre en retrait chaque else if avec la même valeur que la première instruction if, afin que la structure soit claire pour le lecteur :
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("Acidité élevée"));
}
Tabulations et mise en retrait
Veillez à respecter les normes suivantes lors de la mise en retrait du code.
-
Mettez le code en retrait par bloc d'exécution.
-
Plutôt que d'utiliser des espaces, utilisez des tabulations pour insérer des espaces blancs au début d'une ligne. Définissez la taille des tabulations sur 4 caractères. Notez que les espaces sont parfois nécessaires et autorisés pour maintenir le code aligné, quel que soit le nombre d'espaces dans une tabulation. Par exemple, lorsque vous alignez du code qui suit des caractères sans tabulation.
-
Si vous écrivez du code en C++, utilisez également des tabulations et non des espaces. En effet, les programmeurs basculent souvent entre C# et C++, et la plupart d'entre eux préfèrent utiliser un paramètre cohérent pour les tabulations. Par défaut, Visual Studio utilise des espaces pour les fichiers C#. Vous devez donc penser à modifier ce paramètre lorsque vous travaillez sur du code de l'Unreal Engine.
Instructions switch
À l'exception des cas vides (plusieurs cas ayant un code identique), les instructions de cas switch doivent indiquer explicitement qu'un cas passe au cas suivant. Incluez une pause (break) ou un commentaire "falls through" dans chaque cas. D'autres commandes de transfert de contrôle de code (return, continue, etc.) sont également valides.
Veillez à toujours disposer d'un cas par défaut. Incluez une pause (break) juste au cas où une personne ajouterait un nouveau cas après la valeur par défaut.
switch (condition)
{
cas 1:
...
// falls through
cas 2:
...
break;
cas 3:
...
return;
cas 4:
cas 5:
...
break;
default:
break;
}
Espaces de noms
Vous pouvez utiliser des espaces de noms pour organiser vos classes, fonctions et variables, le cas échéant. Dans ce cas, suivez les règles ci-dessous.
-
La majeure partie du code de l'UE n'est actuellement pas enveloppée dans un espace de noms global.
- Veillez à éviter les collisions dans l'étendue globale, en particulier lors de l'utilisation ou de l'inclusion de code tiers.
-
Les espaces de noms ne sont pas pris en charge par UnrealHeaderTool.
- Les espaces de noms ne doivent pas être utilisés lors de la définition de
UCLASSes,USTRUCTs, etc.
- Les espaces de noms ne doivent pas être utilisés lors de la définition de
-
Les nouvelles API qui ne sont pas des
UCLASSes,USTRUCTs, etc., doivent être placées dans un espace de nomsUE::et, idéalement, dans un espace de noms imbriqué, par exemple,UE::Audio::.- Les espaces de noms qui servent à contenir les détails d'implémentation qui ne font pas partie de l'API publique doivent être placés dans un espace de noms
Private, par exemple,UE::Audio::Private::.
- Les espaces de noms qui servent à contenir les détails d'implémentation qui ne font pas partie de l'API publique doivent être placés dans un espace de noms
-
Déclarations
using:- Ne placez pas la déclaration
usingdans l'étendue globale, même dans un fichier.cpp(car cela entraînerait des problèmes avec notre système de build "unity").
- Ne placez pas la déclaration
-
Il est possible de placer les déclarations
usingdans un autre espace de noms ou dans un corps de fonction. -
Si vous placez les déclarations
usingdans un espace de noms, celles-ci seront reportées sur d'autres occurrences de cet espace de noms dans la même unité de traduction. Veillez à rester cohérent pour éviter toute erreur. -
Vous ne pouvez utiliser les déclarations
usingdans les fichiers d'en-tête en toute sécurité que si vous suivez les règles ci-dessus. -
Les types déclarés directement doivent être déclarés dans leur espace de noms respectif.
- Si ce n'est pas le cas, des erreurs de liaison se produisent.
-
Si vous déclarez un grand nombre de classes ou de types dans un espace de noms, il peut être difficile d'utiliser ces types dans d'autres classes d'étendue globale (par exemple, les signatures de fonction doivent utiliser un espace de noms explicite lorsqu'elles figurent dans les déclarations de classe).
-
Vous pouvez utiliser les déclarations
usingafin de créer un alias uniquement pour des variables spécifiques dans un espace de noms dans votre étendue.- Par exemple, en utilisant
Foo::FBar. Cette pratique n'est toutefois pas courante dans le code Unreal.
- Par exemple, en utilisant
-
Les macros ne peuvent pas exister dans un espace de noms.
- Elles doivent être précédées du préfixe
UE_au lieu d'être insérées dans un espace de noms, par exempleUE_LOG.
- Elles doivent être précédées du préfixe
Dépendances physiques
-
Dans la mesure du possible, évitez de préfixer les noms de fichier.
- Par exemple,
Scene.cppau lieu deUScene.cpp. Cela facilite l'utilisation d'outils tels que Workspace Whiz ou Open File in Solution de Visual Assist, en réduisant le nombre de lettres nécessaires pour identifier le fichier souhaité.
- Par exemple,
-
Tous les en-têtes doivent protéger contre les inclusions multiples avec la directive
#pragma once.-
Notez que tous les compilateurs que nous utilisons prennent en charge
#pragma once.#pragma once //<file contents>
-
-
Essayez de minimiser le couplage physique.
- Évitez notamment d'inclure des en-têtes de bibliothèque standard provenant d'autres en-têtes.
-
Préférez les déclarations directes à l'inclusion d'en-têtes.
-
Lorsque vous incluez un en-tête, soyez aussi précis que possible.
- Par exemple, n'incluez pas
Core.h. Au lieu de cela, vous devez inclure les en-têtes spécifiques dans Core dont vous souhaitez extraire les définitions.
- Par exemple, n'incluez pas
-
Essayez d'inclure directement chaque en-tête dont vous avez besoin pour faciliter l'inclusion détaillée.
-
Ne vous fiez pas à un en-tête qui est inclus indirectement par un autre en-tête que vous incluez.
-
Ne comptez pas sur quoi que ce soit qui soit inclus via un autre en-tête. Incluez tout ce dont vous avez besoin.
-
Les modules possèdent des répertoires sources privés et publics.
- Toute définition nécessaire à un autre module doit se trouver dans les en-têtes du répertoire public. Toutes les autres définitions doivent se trouver dans le répertoire privé. Dans les anciens modules Unreal, il est possible que ces répertoires portent toujours le nom "Src" ou "Inc", mais ils sont destinés à séparer le code privé du code public de la même manière. Ils ne sont pas destinés à séparer les fichiers d'en-tête des fichiers sources.
-
Il n'est pas nécessaire que vous configuriez vos en-têtes pour la génération d'en-têtes précompilés.
- Utilisez pour cela UnrealBuildTool, qui effectue cette tâche pour vous.
-
Divisez les fonctions volumineuses en sous-fonctions logiques.
- L'un des domaines d'optimisation du compilateur est l'élimination des sous-expressions communes. Plus vos fonctions sont volumineuses, plus le compilateur doit travailler pour les identifier, ce qui augmente considérablement les délais de création.
-
N'utilisez pas un grand nombre de fonctions en ligne.
- Les fonctions en ligne forcent les reconstructions, même dans les fichiers qui ne les utilisent pas. Les fonctions en ligne ne doivent être utilisées que pour les accesseurs triviaux et lorsque le profilage indique qu'elles sont utiles.
-
Soyez prudent lors de l'utilisation de
FORCEINLINE.- Le code et les variables locales seront développés dans la fonction appelante. Cela entraînera les mêmes problèmes de délais de création que ceux causés par des fonctions volumineuses.
Encapsulation
Appliquez l'encapsulation avec les mots-clés de protection. Les membres de classe doivent presque toujours être déclarés comme étant privés, à moins qu'ils fassent partie de l'interface publique/protégée de la classe. Faites preuve de bon sens, mais gardez toujours à l'esprit qu'un manque d'accesseurs rend difficile la refactorisation ultérieure sans bloquer les plug-ins et le projet existant.
Si des champs particuliers sont uniquement destinés à être utilisés par une classe dérivée, rendez-les privés et fournissez des accesseurs protégés.
Utilisez final si votre classe n'est pas conçue pour être dérivée.
Problèmes généraux de style
-
Minimisez la distance de dépendance.
- Lorsque le code dépend d'une certaine valeur d'une variable, essayez de définir la valeur de cette variable juste avant de l'utiliser. Si vous initialisez une variable au début d'un bloc d'exécution et ne l'utilisez pas pendant une centaine de lignes de code, il est fort probable qu'une personne change accidentellement la valeur sans se rendre compte de la dépendance. Le fait d'indiquer la variable à la ligne suivante permet de comprendre pourquoi elle est initialisée de cette manière et où elle est utilisée.
-
Divisez les méthodes en sous-méthodes lorsque cela est possible.
- Il est plus facile d'obtenir une vue d'ensemble, puis de se concentrer sur les détails intéressants, que de commencer par les détails pour tenter d'avoir une vue d'ensemble. De la même manière, il est plus facile de comprendre une méthode simple, qui appelle une séquence de diverses sous-méthodes correctement nommées, que de comprendre une méthode équivalente contenant simplement l'ensemble du code de ces sous-méthodes.
-
Dans les sites de déclaration de fonction ou d'appel de fonction, n'ajoutez pas d'espace entre le nom de la fonction et les parenthèses qui précèdent la liste des arguments.
-
Prêtez attention aux avertissements du compilateur.
- Les messages d'avertissement du compilateur indiquent qu'une erreur s'est produite. Veillez par conséquent à corriger les erreurs que vous signale le compilateur. Si vous ne pouvez absolument pas résoudre ces erreurs, utilisez
#pragmapour supprimer l'avertissement, mais cela ne doit être fait qu'en dernier recours.
- Les messages d'avertissement du compilateur indiquent qu'une erreur s'est produite. Veillez par conséquent à corriger les erreurs que vous signale le compilateur. Si vous ne pouvez absolument pas résoudre ces erreurs, utilisez
-
Laissez une ligne vierge à la fin du fichier.
- Tous les fichiers
.cppet.hdoivent inclure une ligne vierge pour se conformer à gcc.
- Tous les fichiers
-
Le code de débogage doit être utile et précis, ou ne pas être archivé.
- Tout code de débogage mélangé à un autre code rend l'autre code plus difficile à lire.
-
Utilisez toujours la macro
TEXT()autour des littéraux de chaîne.- Sans la macro
TEXT(), le code qui construitFStringsà partir de littéraux entraîne un processus de conversion de chaîne indésirable.
- Sans la macro
-
Évitez de répéter en boucle la même opération de façon redondante.
- Déplacez les sous-expressions communes hors des boucles pour éviter les calculs redondants. Utilisez les statiques dans certains cas, afin d'éviter les opérations globalement redondantes lors d'appels de fonction, comme la construction d'un
FNameà partir d'un littéral de chaîne.
- Déplacez les sous-expressions communes hors des boucles pour éviter les calculs redondants. Utilisez les statiques dans certains cas, afin d'éviter les opérations globalement redondantes lors d'appels de fonction, comme la construction d'un
-
Portez une attention particulière à la compilation à chaud.
- Minimisez les dépendances pour réduire le délai d'itération. N'utilisez pas l'extension inline ou les modèles pour les fonctions susceptibles de changer lors d'un rechargement. N'utilisez les statiques que pour les éléments censés rester constants lors d'un rechargement.
-
Utilisez des variables intermédiaires pour simplifier les expressions complexes.
-
Si vous avez une expression complexe, vous pouvez la rendre plus facile à comprendre si vous la divisez en sous-expressions, qui sont affectées à des variables intermédiaires, avec des noms décrivant la signification de la sous-expression dans l'expression parente. Par exemple :
if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
Doit être remplacé par :
-
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
si (bIsLegalWindow && !bIsPlayerDead)
{
DoSomething();
}
-
Les pointeurs et les références ne doivent comporter qu'un seul espace à droite du pointeur ou de la référence.
-
Cela permet d'utiliser rapidement et facilement Rechercher dans les fichiers pour tous les pointeurs ou références vers un certain type. Par exemple :
// Utiliser FShaderType* Ptr // Ne pas utiliser : FShaderType *Ptr FShaderType * Ptr
-
-
Les variables masquées ne sont pas autorisées.
-
C++ permet de masquer une variable à partir d'une étendue externe, mais cela rend l'utilisation ambiguë pour le lecteur. Par exemple, cette fonction membre comporte trois variables
Countutilisables :class FSomeClass { public: void Func(const int32 Count) { pour (int32 Count = 0; Count != 10; ++Count) { // Utiliser Count } } private: int32 Count; }
-
-
Évitez d'utiliser des littéraux anonymes dans les appels de fonction.
-
Préférez les constantes nommées qui décrivent leur signification. Cela rend l'intention plus évidente pour un lecteur occasionnel, car cela évite d'avoir à rechercher la déclaration de fonction pour la comprendre.
// Ancien style Trigger(TEXT("Soldier"), 5, true);. // Nouveau style const FName ObjectName = TEXT("Soldat"); const float CooldownInSeconds = 5; const bool bVulnerableDuringCooldown = true; Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
-
-
Évitez de définir des variables statiques non triviales dans les en-têtes.
-
Les variables statiques non triviales entraînent la compilation d'une instance à compiler dans chaque unité de traduction qui inclut cet en-tête :
// SomeModule.h static const FString GUsefulNamedString = TEXT("String"); // *Remplacez ce qui précède par :* // SomeModule.h extern SOMEMODULE_API const FString GUsefulNamedString; // SomeModule.cpp const FString GUsefulNamedString = TEXT("String");
-
-
Évitez d'effectuer de vastes modifications qui ne changent pas le comportement du code (par exemple : modifier des espaces blancs ou renommer en masse des variables privées), car elles provoquent un bruit inutile dans l'historique de la source et peuvent entraîner des perturbations lors de la fusion.
-
Si cette modification est importante, par exemple la correction d'une mise en retrait rompue due à un outil de fusion automatisé, elle doit être soumise seule et ne pas être mélangée à des modifications de comportement.
-
Il est préférable de corriger les espaces blancs ou autres violations mineures des normes de codage uniquement lorsque d'autres modifications sont apportées aux mêmes lignes ou au code voisin.
-
Instructions de conception d'API
-
Évitez les paramètres de fonctions booléens.
-
En particulier, les paramètres booléens sont à éviter pour les indicateurs transmis aux fonctions. Ceux-ci présentent le même problème de littéral anonyme que celui mentionné précédemment, mais ont également tendance à se multiplier à mesure que les API se développent. Préférez une énumération (voir les conseils d'utilisation des énumérations en tant qu'indicateurs dans la section Énumérations fortement typées) :
// Ancien style FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false); FCup* Cup = MakeCupOfTea(Tea, false, true, true); // Nouveau style enum class ETeaFlags { None, Lait = 0x01, Sucre = 0x02, Honey = 0x04, Citron = 0x08 }; ENUM_CLASS_FLAGS(ETeaFlags) FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None); FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
-
-
Cette forme empêche la transposition accidentelle des indicateurs, évite la conversion accidentelle à partir d'arguments de pointeur et d'entier, supprime la nécessité de répéter des valeurs par défaut redondantes et présente un gain d'efficacité.
-
Il est acceptable d'utiliser
boolsen tant qu'arguments lorsqu'ils constituent l'état terminé à transmettre à une fonction telle qu'un setter, par exemple :void FWidget::SetEnabled(bool bEnabled). Envisagez cependant une refactorisation en cas de modification. -
Évitez les listes de paramètres de fonction trop longues.
-
Lorsqu'une fonction utilise de nombreux paramètres, envisagez plutôt de transmettre une structure dédiée :
// Ancien style TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f); // Nouveau style 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);
-
-
Évitez de surcharger les fonctions avec
booletFString.-
Cela peut provoquer un comportement inattendu :
void Func(const FString& String); void Func(bool bBool); Func(TEXT("String")); // Appelle la surcharge bool !
-
-
Les classes d'interface doivent toujours être abstraites.
- Les classes d'interface sont précédées du préfixe "I" et ne doivent pas avoir de variables membres. Les interfaces peuvent contenir des méthodes qui ne sont pas purement virtuelles et peuvent contenir des méthodes qui ne sont pas virtuelles ou statiques, à condition qu'elles soient implémentées en ligne.
-
Utilisez les mots-clés
virtualetoverridelorsque vous déclarez une méthode de remplacement.
Lors de la déclaration d'une fonction virtuelle dans une classe dérivée qui remplace une fonction virtuelle dans la classe parente, vous devez utiliser à la fois les mots-clés virtual et override. Par exemple :
class A
{
public:
virtual void F() {}
};
class B : public A
{
public:
virtual void F() override;
}
Une grande partie du code existant ne suit pas encore cette règle, en raison de l'ajout récent du mot-clé override. Veillez à ajouter le mot-clé override à ce code lorsque cela s'avère nécessaire.
-
Les UObjects doivent être transmis par pointeur et non par référence. Si null n'est pas attendu par une fonction, cela doit être documenté par l'API ou géré de manière appropriée. Par exemple :
// Mauvais void AddActorToList(AActor& Obj); // Correct void AddActorToList(AActor* Obj);
Code spécifique à la plateforme
Le code propre à une plateforme doit toujours être abstrait et implémenté dans un fichier source spécifique à la plateforme dans des sous-répertoires nommés de manière appropriée, par exemple :
Engine/Platforms/[PLATFORM]/Source/Runtime/Core/Private/[PLATFORM]PlatformMemory.cpp
En général, vous devez éviter d'ajouter toute utilisation de PLATFORM_[PLATFORM]. Par exemple, évitez d'ajouter PLATFORM_XBOXONE pour coder en dehors d'un répertoire nommé [PLATFORM]. Au lieu de cela, étendez la couche d'abstraction matérielle pour ajouter une fonction statique, par exemple dans FPlatformMisc :
FORCEINLINE static int32 GetMaxPathLength()
{
return 128;
}
Les plateformes peuvent alors remplacer cette fonction, en renvoyant une valeur constante propre à la plateforme ou en utilisant les API de la plateforme pour déterminer le résultat. Si vous forcez l'insertion de la fonction, elle présente les mêmes caractéristiques de performances que l'utilisation d'une définition.
Dans les cas où une définition est absolument nécessaire, créez de nouvelles directives #define qui décrivent des propriétés particulières pouvant s'appliquer à une plateforme, par exemple PLATFORM_USE_PTHREADS. Définissez la valeur par défaut dans Platform.h et remplacez-la pour toute plateforme qui en a besoin dans le fichier Platform.h spécifique à la plateforme.
Par exemple, dans Platform.h, nous avons :
#ifndef PLATFORM_USE_PTHREADS
#define PLATFORM_USE_PTHREADS 1
#endif
WindowsPlatform.h a :
#define PLATFORM_USE_PTHREADS 0
Le code multiplateforme peut alors utiliser la définition directement sans avoir besoin de connaître la plateforme.
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
Nous centralisons les détails spécifiques à la plateforme du moteur, ce qui permet d'englober la totalité des détails dans les fichiers sources propres à la plateforme. Cela facilite la maintenance du moteur sur plusieurs plateformes. De plus, vous pouvez transférer le code vers de nouvelles plateformes sans avoir à parcourir la base de code pour rechercher des définitions propres à la plateforme.
Conserver le code de la plateforme dans des dossiers spécifiques à la plateforme est également une exigence pour les plateformes NDA telles que PlayStation, Xbox et Nintendo Switch.
Il est important de s'assurer que le code se compile et s'exécute, que le sous-répertoire [PLATFORM] soit présent ou pas. En d'autres termes, le code multiplateforme ne doit jamais dépendre du code propre à la plateforme.