Les règles de cette section sont très générales.
Résumé des règles philosophiques :
Les règles philosophiques ne sont généralement pas vérifiables mécaniquement. Cependant, les règles individuelles reflétant ces thèmes philosophiques le sont. Sans base philosophique, les règles plus concrètes / spécifiques / vérifiables manquent de justification.
Les compilateurs ne lisent pas les commentaires (ou les documents de conception) et de même beaucoup de programmeurs ne le font pas (de façon constante). Ce qui est exprimé dans le code possède une sémantique définie et peut (en principe) être vérifié par les compilateurs et d’autres outils.
class Date {
public:
Month month() const; // à faire
int month(); // à ne pas faire
// ...
};
La première déclaration de month indique explicitement qu’elle renvoie un Month et qu’elle ne modifie pas l’état de l’objet Date.
La seconde version laisse le lecteur deviner et ouvre davantage de possibilités de bogues non détectés.
Cette boucle est une forme restreinte de std::find :
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
int index = -1; // mauvais, de plus devrait utiliser gsl::index
for (int i = 0; i < v.size(); ++i) {
if (v[i] == val) {
index = i;
break;
}
}
// ...
}
Une expression beaucoup plus claire de l’intention serait :
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
auto p = find(begin(v), end(v), val); // meilleur
// ...
}
Une bibliothèque bien conçue exprime l’intention (ce qui doit être fait, plutôt que simplement comment quelque chose est fait) bien mieux que l’utilisation directe des caractéristiques du langage.
Un programmeur C++ devrait connaître les bases de la bibliothèque standard et l’utiliser quand il le faut. Tout programmeur devrait connaître les bases des bibliothèques fondamentales du projet sur lequel il travaille et les utiliser de manière appropriée. Tout programmeur qui suit ces lignes directrices devrait connaître la bibliothèque d’assistance aux lignes directrices @TODO‑LINK et l’utiliser correctement.
change_speed(double s); // mauvais : que signifie s ?
// ...
change_speed(2.3);
Une meilleure approche consiste à être explicite quant à la signification du double (nouvelle vitesse ou variation de la vitesse ?) et à l’unité utilisée :
change_speed(Speed s); // meilleur : la signification de s est spécifiée
// ...
change_speed(2.3); // erreur : aucune unité
change_speed(23_m / 10s); // mètres par seconde
Nous aurions pu accepter un double « nu » comme variation, mais cela aurait été source d’erreurs.
Si nous voulions à la fois la vitesse absolue et les variations, nous aurions défini un type Delta.
Très difficile en général.
const de façon cohérente (vérifier si les fonctions membres modifient leur objet ; vérifier si les fonctions modifient les arguments passés par pointeur ou référence)Il s’agit d’un ensemble de lignes directrices pour écrire du C++ ISO standard.
Il existe des environnements où les extensions sont nécessaires, par exemple pour accéder à des ressources système. Dans ces cas, il faut localiser l’usage des extensions requises et contrôler leur utilisation avec des « Coding Guidelines » non‑core. Si possible, concevez des interfaces qui encapsulent les extensions afin qu’elles puissent être désactivées ou compilées hors du code sur les systèmes qui ne les supportent pas.
Les extensions n’ont souvent pas de sémantique rigoureusement définie. Même les extensions courantes et implémentées par plusieurs compilateurs peuvent présenter des comportements légèrement différents et des cas limites en raison du manque d’une définition normative stricte. Une utilisation suffisante de l’une de ces extensions affectera la portabilité attendue.
Utiliser du C++ ISO valide ne garantit pas la portabilité (et encore moins la correction).
Évitez de dépendre d’un comportement indéfini (par ex. : ordre d’évaluation indéfini) et soyez conscient des constructions à signification d’implémentation (par ex. : sizeof(int)).
Il existe des environnements où des restrictions sur l’usage des fonctionnalités du langage C++ standard ou de la bibliothèque standard sont nécessaires, par exemple pour éviter l’allocation dynamique de mémoire imposée par les standards de logiciels de commande d’avion. Dans ces cas, contrôlez leur (dés)utilisation avec une extension de ces « Coding Guidelines » adaptée à l’environnement spécifique.
Utilisez un compilateur C++ à jour (actuellement C++20 ou C++17) avec un ensemble d’options qui n’acceptent pas les extensions.
À moins que l’intention d’un morceau de code ne soit indiquée (par des noms ou des commentaires), il est impossible de savoir si le code fait ce qu’il est censé faire.
gsl::index i = 0;
while (i < v.size()) {
// ... faire quelque chose avec v[i] ...
}
L’intention de « simplement » parcourir les éléments de v n’est pas exprimée ici. Le détail d’implémentation d’un indice est exposé (et pourrait être mal utilisé), et i dépasse la portée de la boucle, ce qui peut ou non être prévu. Le lecteur ne peut pas le savoir à partir de ce seul extrait de code.
Meilleur :
for (const auto& x : v) { /* faire quelque chose avec la valeur de x */ }
Ici, il n’y a aucune mention explicite du mécanisme d’itération, et la boucle travaille sur une référence à des éléments const afin d’éviter toute modification accidentelle. Si la modification est voulue, indiquez‑le :
for (auto& x : v) { /* modifier x */ }
Pour plus de détails sur les instructions for, voir ES.71.
Parfois il est encore préférable d’utiliser un algorithme nommé. Cet exemple utilise for_each de la Ranges TS car il exprime directement l’intention :
for_each(v, [](int x) { /* faire quelque chose avec la valeur de x */ });
for_each(par, v, [](int x) { /* faire quelque chose avec la valeur de x */ });
La dernière variante montre clairement que l’ordre de traitement des éléments de v n’a pas d’importance.
Un programmeur devrait être familier avec :
Formulation alternative : dire ce qui doit être fait plutôt que simplement comment le faire.
Certains construits du langage expriment mieux l’intention que d’autres.
Si deux int doivent représenter les coordonnées d’un point 2D, indiquez‑le :
draw_line(int, int, int, int); // obscur : (x1,y1,x2,y2) ? (x,y,h,w) ? … ?
// il faut consulter la documentation pour savoir
draw_line(Point, Point); // plus clair
Rechercher les motifs courants pour lesquels il existe de meilleures alternatives :
for simples vs. boucles range‑forf(T*, int) vs. interfaces f(span<T>)new et delete nusIl existe un vaste champ de possibilités pour des transformations de programme semi‑automatisées.
Idéalement, un programme serait entièrement sûr du typage à la compilation. Malheureusement, ce n’est pas toujours possible. Zones problématiques :
Ces zones sont à l’origine de problèmes graves (par ex. : plantages et violations de sécurité). Nous essayons de fournir des techniques alternatives.
Nous pouvons interdire, restreindre ou détecter les catégories de problèmes individuellement, selon les exigences et la faisabilité pour chaque programme. Toujours proposer une alternative :
variant (C++17)templates) peuvent aiderspan (de la GSL)spannarrow ou narrow_cast (de la GSL)Clarté du code et performances. Vous n’avez pas besoin d’écrire des gestionnaires d’erreurs pour des erreurs déjà détectées à la compilation.
// Int est un alias utilisé pour les entiers
int bits = 0; // mauvais : code évitable
for (Int i = 1; i; i <<= 1)
++bits;
if (bits < 32)
cerr << "Int too small\n";
Cet exemple n’atteint pas son objectif (car le dépassement de capacité est indéfini) et devrait être remplacé par un simple static_assert :
// Int est un alias utilisé pour les entiers
static_assert(sizeof(Int) >= 4); // faire : vérification à la compilation
Ou, encore mieux, utilisez simplement le système de types et remplacez Int par int32_t.
void read(int* p, int n); // lire au maximum n entiers dans *p
int a[100];
read(a, 1000); // mauvais, hors limites
Meilleur :
void read(span<int> r); // lire dans la plage d'entiers r
int a[100];
read(a); // meilleur : laisser le compilateur déterminer le nombre d'éléments
Formulation alternative : ne remettez pas à l’exécution ce qui peut être fait à la compilation.
Laisser des erreurs difficiles à détecter dans un programme, c’est inviter les plantages et les résultats incorrects.
Idéalement, nous attrapons toutes les erreurs (qui ne découlent pas d’une erreur de logique du programmeur) soit à la compilation, soit à l’exécution. Il est impossible de toutes les attraper à la compilation et souvent trop coûteux de toutes les attraper à l’exécution. Néanmoins, nous devrions nous efforcer d’écrire des programmes qui, en principe, peuvent être vérifiés, à condition de disposer de ressources suffisantes (outils d’analyse, vérifications d’exécution, ressources machines, temps).
// compilation séparée, éventuellement chargée dynamiquement
extern void f(int* p);
void g(int n)
{
// mauvais : le nombre d'éléments n'est pas transmis à f()
f(new int[n]);
}
Ici, une information cruciale (le nombre d’éléments) est tellement « cachée » que l’analyse statique devient probablement impossible et la vérification dynamique très difficile lorsque f() fait partie d’une ABI que l’on ne peut pas « instrumenter ». On pourrait incorporer des informations utiles dans le tas, mais cela nécessite des changements globaux du système, voire du compilateur. Le design proposé rend la détection d’erreurs très ardue.
// compilation séparée, éventuellement chargée dynamiquement
extern void f2(int* p, int n);
void g2(int n)
{
// mauvais : le mauvais nombre d'éléments peut être passé à f2()
f2(new int[n], n);
}
Passer le nombre d’éléments en argument est meilleur (et bien plus répandu) que de ne transmettre que le pointeur et de se reposer sur une convention (non déclarée) pour connaître ou découvrir le nombre d’éléments. Cependant, une simple faute de frappe peut introduire une erreur grave. La connexion entre les deux arguments de f2() est conventionnelle, pas explicite.
De plus, il est implicite que f2() doit delete son argument (ou bien le appelant a commis une seconde erreur ?).
// compilation séparée, éventuellement chargée dynamiquement
// NB : cela suppose que le code appelant est compatible ABI, utilise un
// compilateur C++ compatible et la même implémentation de la bibliothèque standard
extern void f3(unique_ptr<int[]>, int n);
void g3(int n)
{
f3(make_unique<int[]>(n), m); // mauvais : passer la possession et la taille séparément
}
// compilation séparée, éventuellement chargée dynamiquement
extern void f4(vector<int>&); // compilation séparée, éventuellement chargée dynamiquement
extern void f4(span<int>); // compilation séparée, éventuellement chargée dynamiquement
// NB : cela suppose que le code appelant est compatible ABI,
// utilise un compilateur C++ compatible et la même implémentation de la bibliothèque standard
void g3(int n)
{
vector<int> v(n);
f4(v); // passer par référence, conserver la possession
f4(span<int>{v}); // passer une vue, conserver la possession
}
Ce design transporte le nombre d’éléments comme partie intégrante d’un objet, de sorte que les erreurs sont peu probables et que la vérification d’exécution demeure toujours possible, même si elle n’est pas toujours abordable.
vector<int> f5(int n) // OK : déplacement
{
vector<int> v(n);
// ... initialiser v ...
return v;
}
unique_ptr<int[]> f6(int n) // mauvais : perd n
{
auto p = make_unique<int[]>(n);
// ... initialiser *p ...
return p;
}
owner<int*> f7(int n) // mauvais : perd n et on pourrait oublier de libérer
{
owner<int*> p = new int[n];
// ... initialiser *p ...
return p;
}
Éviter les plantages « mystérieux ». Éviter les erreurs menant à des résultats (potentiellement) incorrects et non reconnus.
void increment1(int* p, int n) // mauvais : sujet à erreur
{
for (int i = 0; i < n; ++i) ++p[i];
}
void use1(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment1(a, m); // peut-être une faute de frappe, peut-être que m <= n était supposé
// mais supposons que m == 20
// ...
}
Nous avons ici introduit une petite erreur dans use1 qui conduit à des données corrompues ou à un plantage.
L’interface de type (pointeur, comptage) laisse increment1() sans moyen réaliste de se défendre contre les accès hors‑borne. Si nous pouvions vérifier les indices dès l’accès, l’erreur ne serait découverte que lorsque p[10] serait accédé. Nous pouvons vérifier plus tôt et améliorer le code :
void increment2(span<int> p)
{
for (int& x : p) ++x;
}
void use2(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2({a, m}); // peut-être une faute de frappe, peut-être que m <= n était supposé
// ...
}
Maintenant, la condition m <= n peut être vérifiée au moment de l’appel (tôt) plutôt que plus tard.
Si la faute de frappe était que nous voulions réellement utiliser n comme borne, le code peut être simplifié davantage (en éliminant la possibilité d’erreur) :
void use3(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2(a); // le nombre d'éléments de a n'a pas besoin d'être répété
// ...
}
Ne pas vérifier deux fois la même valeur. Ne pas passer des données structurées sous forme de chaînes :
Date read_date(istream& is); // lire une date depuis le flux d'entrée
Date extract_date(const string& s); // extraire une date depuis une chaîne
void user1(const string& date) // manipuler la date
{
auto d = extract_date(date);
// ...
}
void user2()
{
Date d = read_date(cin);
// ...
user1(d.to_string());
// ...
}
La date est validée deux fois (par le constructeur Date) et transmise sous forme de chaîne de caractères (données non structurées).
Un excès de vérifications peut être coûteux. Il existe des cas où la vérification précoce est inefficace parce que vous pourriez ne jamais avoir besoin de la valeur, ou ne nécessiter qu’une partie de la valeur qui est plus facilement vérifiable que l’ensemble. De même, n’ajoutez pas de vérifications de validité qui changent le comportement asymptotique de votre interface (par ex. : n’ajoutez pas une vérification O(n) à une interface dont la complexité moyenne est O(1)) :
class Jet { // La physique dit : e * e < x * x + y * y + z * z
float x;
float y;
float z;
float e;
public:
Jet(float x, float y, float z, float e)
:x(x), y(y), z(z), e(e)
{
// Dois‑je vérifier ici que les valeurs sont physiquement plausibles ?
}
float m() const
{
// Dois‑je gérer le cas dégénéré ici ?
return sqrt(x * x + y * y + z * z - e * e);
}
???
};
La loi physique pour un jet (e * e < x * x + y * y + z * z) n’est pas un invariant à cause des possibles erreurs de mesure.
???
Même une croissance lente des ressources épuisera, à terme, la disponibilité de ces ressources. C’est particulièrement important pour les programmes de longue durée, mais constitue aussi un principe essentiel de programmation responsable.
void f(const char* name)
{
FILE* input = fopen(name, "r");
// ...
if (something) return; // mauvais : si something == true, le descripteur de fichier est perdu
// ...
fclose(input);
}
Préférez RAII :
void f(const char* name)
{
ifstream input {name};
// ...
if (something) return; // OK : aucune fuite
// ...
}
Voir aussi : La section de gestion des ressources
Une fuite est, familièrement, « tout ce qui n’est pas nettoyé ». La classification la plus importante est « tout ce qui ne peut plus être nettoyé ». Par exemple, allouer un objet sur le tas puis perdre le dernier pointeur qui pointe vers cette allocation.
Cette règle ne doit pas être interprétée comme imposant que les allocations au sein d’objets de longue durée soient libérées lors de l’arrêt du programme. Par exemple, compter sur le nettoyage garanti par le système (fermeture de fichiers, désallocation de mémoire à la fin du processus) peut simplifier le code. Néanmoins, se reposer sur des abstractions qui nettoient implicitement peut être tout aussi simple – et souvent plus sûr.
Faire respecter le profil de sécurité du cycle de vie élimine les fuites. Lorsqu’il est combiné avec la sécurité des ressources fournie par RAII, il élimine le besoin de « garbage collection » (en générant aucun déchet). Combinez cela avec l’application des profils de type et de limites et vous obtenez une sécurité totale du type et des ressources, garantie par les outils.
owner de la GSL @TODO‑LINK.new et delete nusfopen, malloc ou strdup)C’est du C++.
Le temps et l’espace que vous consacrez judicieusement à atteindre un but (par ex. : rapidité de développement, sécurité des ressources ou simplification des tests) ne sont pas du gaspillage. « Un autre avantage de viser l’efficacité est que le processus vous oblige à comprendre le problème plus en profondeur. » – Alex Stepanov
struct X {
char ch;
int i;
string s;
char ch2;
X& operator=(const X& a);
X(const X&);
};
X waste(const char* p)
{
if (!p) throw Nullptr_error{};
int n = strlen(p);
auto buf = new char[n];
if (!buf) throw Allocation_error{};
for (int i = 0; i < n; ++i) buf[i] = p[i];
// ... manipuler le tampon ...
X x;
x.ch = 'a';
x.s = string(n); // donner à x.s l'espace pour *p
for (gsl::index i = 0; i < x.s.size(); ++i) x.s[i] = buf[i]; // copier buf dans x.s
delete[] buf;
return x;
}
void driver()
{
X x = waste("Typical argument");
// ...
}
Oui, c’est une caricature, mais chaque erreur individuelle a déjà été observée dans du code en production, voire pire.
Notez que la disposition de X garantit qu’au moins 6 octets (et très probablement davantage) sont gaspillés.
La définition redondante des opérations de copie désactive les sémantiques de déplacement, ce qui rend l’opération de retour lente (notez que la Return Value Optimization (RVO) n’est pas garantie ici).
L’utilisation de new et delete pour buf est superflue ; si nous avions réellement besoin d’une chaîne locale, il aurait fallu utiliser std::string.
Il existe plusieurs autres bugs de performance et complexités inutiles.
void lower(zstring s)
{
for (int i = 0; i < strlen(s); ++i) s[i] = tolower(s[i]);
}
C’est en fait un extrait de code de production.
Nous voyons que dans notre condition nous avons i < strlen(s). Cette expression est évaluée à chaque itération de la boucle, ce qui oblige strlen à parcourir la chaîne à chaque tour pour en connaître la longueur.
Comme le contenu de la chaîne change, on suppose que tolower n’affecte pas la longueur, il vaut donc mieux mettre la longueur en cache avant la boucle afin d’éviter ce coût à chaque itération.
Un exemple isolé de gaspillage est rarement significatif ; lorsqu’il l’est, il est généralement éliminable par un expert. En revanche, le gaspillage largement répandu dans une base de code peut rapidement devenir conséquent, et les experts ne sont pas toujours disponibles. L’objectif de cette règle (et des règles plus spécifiques qui la soutiennent) est d’éliminer la plupart du gaspillage lié à l’usage de C++ avant même qu’il ne survienne. Ensuite, on pourra s’occuper du gaspillage lié aux algorithmes et aux exigences, ce qui dépasse le cadre de ces lignes directrices.
De nombreuses règles plus spécifiques visent les objectifs globaux de simplicité et d’élimination du gaspillage gratuit.
operator++ ou operator-- postfixe définie par l’utilisateur et non‑défaultée. Préférez la forme préfixe. (Remarque : « non‑défaulté » a pour but de réduire le bruit. Réévaluez cette application si elle reste trop bruyante en pratique.)Il est plus facile de raisonner sur des constantes que sur des variables. Quelque chose d’immuable ne peut pas changer de façon inattendue. Parfois, l’immuabilité permet de meilleures optimisations. On ne peut pas rencontrer de data race sur une constante.
Voir Con : Constantes et immuabilité @TODO‑LINK.
Le code désordonné est plus susceptible de masquer des bugs et plus difficile à écrire. Une bonne interface est plus facile et plus sûre à utiliser. Le code bas‑niveau, désordonné, engendre davantage de code de ce type.
int sz = 100;
int* p = (int*) malloc(sizeof(int) * sz);
int count = 0;
// ...
for (;;) {
// ... lire un int dans x, sortir de la boucle si fin de fichier atteinte ...
// ... vérifier que x est valide ...
if (count == sz)
p = (int*) realloc(p, sizeof(int) * sz * 2);
p[count++] = x;
// ...
}
C’est du bas‑niveau, verbeux et sujet à erreurs. Par exemple, nous avons « oublié » de tester l’épuisement de mémoire et d’ajuster la valeur de sz.
À la place, nous pourrions utiliser vector :
vector<int> v;
v.reserve(100);
// ...
for (int x; cin >> x; ) {
// ... vérifier que x est valide ...
v.push_back(x);
}
La bibliothèque standard et la GSL sont des exemples de cette philosophie.
Par exemple, au lieu de bricoler avec des tableaux, des unions, des casts, des problèmes de durée de vie délicats, gsl::owner, etc., qui sont nécessaires pour implémenter des abstractions clés comme vector, span, lock_guard ou future, nous utilisons les bibliothèques conçues et implémentées par des personnes disposant de plus de temps et d’expertise que nous n’en avons généralement.
De même, nous pouvons et devons concevoir et implémenter des bibliothèques plus spécialisées, plutôt que de laisser les utilisateurs (souvent nous‑mêmes) relever le défi de réécrire du code bas‑niveau maintes fois.
Il s’agit d’une variante du principe du sous‑ensemble du sur‑ensemble qui sous‑tend ces lignes directrices.
De nombreuses tâches sont mieux réalisées « par la machine ». Les ordinateurs ne se fatiguent pas et ne s’ennuient pas avec les tâches répétitives. Nous avons généralement de meilleures choses à faire que d’exécuter des tâches de routine à la main.
Exécuter un analyseur statique pour vérifier que votre code suit les lignes directrices que vous souhaitez respecter.
Voir :
Il existe de nombreux autres types d’outils, comme les systèmes de contrôle de version, les outils de construction, etc., mais ceux‑ci dépassent le champ de ces lignes directrices.
Soyez prudent de ne pas devenir dépendant de chaînes d’outils trop élaborées ou trop spécialisées. Celles‑ci peuvent rendre votre code, autrement portable, non‑portable.
Utiliser une bibliothèque bien conçue, bien documentée et bien supportée fait gagner du temps et des efforts ; sa qualité et sa documentation sont probablement supérieures à ce que vous pourriez atteindre si la majorité de votre temps devait être consacrée à l’implémentation. Le coût (temps, effort, argent, etc.) d’une bibliothèque peut être partagé entre de nombreux utilisateurs. Une bibliothèque largement utilisée a plus de chances d’être maintenue à jour et portée sur de nouveaux systèmes qu’une application individuelle. La connaissance d’une bibliothèque largement utilisée peut faire gagner du temps sur d’autres projets futurs. Ainsi, si une bibliothèque adaptée à votre domaine d’application existe, utilisez‑la.
std::sort(begin(v), end(v), std::greater<>());
À moins d’être un expert en algorithmes de tri et de disposer de beaucoup de temps, cela est plus susceptible d’être correct et plus rapide que tout ce que vous pourriez écrire vous‑même pour une application spécifique. Vous avez besoin d’une raison de ne pas utiliser la bibliothèque standard (ou quelles que soient les bibliothèques fondamentales que votre application utilise) plutôt que d’une raison de l’utiliser.
Par défaut, utilisez :
Si aucune bibliothèque bien conçue, bien documentée et bien supportée n’existe pour un domaine important, peut‑être devrait‑vous la concevoir et l’implémenter, puis l’utiliser.