Florilège de petits trucs

Quelques raccourcis :

Il existe des « trucs scouts » tout petits et tout simples, pour lesquels un article plein et entier peut ne pas être nécessaire (ou, en toute honnêteté, pour lesquels je n'ai pas encore eu le temps d'écrire un article). C'est ce type de petit truc que je dépose dans cet « article ». Notez qu'il est probable que certains d'entre eux aient éventuellement une « promotion » et deviennent un article à part entière, si le besoin s'en manifeste... et si j'en ai le temps.

Dans tous les cas, on parle de trucs et de conseils, pas de dogmes. Si vous avez une bonne raison de ne pas les respecter, alors faites, mais documentez le tout.

Généralités

Quelques trucs d'ordre général.

Que faire lorsqu'on est « bloqué(e) »

Supposons que vous soyez l'un(e) de mes étudiant(e)s et que, dans le cadre de l'un de vos travaux, personnel ou pour l'un de mes cours, vous soyez « bloqué(e) ». Que faire? Dans l'ordre :

Questions d'orthographe et de grammaire

Oui, ça compte. Si je ne vous comprends pas, votre note ne peut que descendre. Visez des explications claires, des phrases complètes. évitez les anglicismes, les erreurs en genre et en nombre. Demandez à un tiers de vous relire. Profitez des outils à votre disposition, incluant vos correcteurs, orthographique comme grammatical. C'est à votre avantage. Après tout, en oeuvrant en informatique, vous avez fait la preuve de votre capacité à apprendre plusieurs langues. Prenez autant soin de votre français écrit que de votre code!

Indenter : espaces ou tabulations?

N'indentez pas avec des tabulations.

Journalisation facile

En C++, il existe quatre flux standards, soit :

Sans être sophistiqué, std::clog permet de réaliser une journalisation simple du fait qu'il s'agit d'un flux standard globalement accessible dans un programme. Ce qui suit donne un exemple d'utilisation de ce flux.

Le flux std::clog est déclaré à même <iostream>, qui est un en-tête assez lourd, donc essayez de ne l'utiliser que dans un .cpp pour éviter de ralentir la compilation de vos programmes de manière générale. En pratique, on associera la sortie de std::clog à un flux en sortie de notre choix, ce qui explique le recours à <fstream>.

#include <iostream>
#include <fstream>
using namespace std;

On utilisera std::clog comme un utilise n'importe quel flux en sortie.

void f(int n) {
   clog << "Dans f(" << n << ")!" << endl;
}

Le programme de test à droite montre comment associer std::clog à une sortie particulière (ici, le fichier log.txt) : on ouvre un flux en sortie, puis on associe le tampon interne de std::clog à celui du flux en question.

Il est possible de faire en sorte que std::clog n'écrive rien (sorte de /dev/null) en l'associant au « tampon » qu'est nullptr. Ainsi, le programme à droite écrira ce qui suit dans le fichier log.txt :

Dans f(3)!
int main() {
   ofstream out{ "log.txt" };
   clog.rdbuf(out.rdbuf());
   f(3);
   clog.rdbuf(nullptr);
   f(4);
}

Approche OO

Quelques conseils brefs quant à la mise en application de l'approche orientée objet.

Rôle des constructeurs

Voyez le constructeur comme le lieu de l'initialisation des états d'un objet. Ne placez pas tout votre programme dans un constructeur; ça ne donnera pas du meilleur code.

Un objet nouvellement construit doit être utilisable pour que l'encapsulation soit correcte et pour que l'objet puisse garantir ses invariants. Assurez-vous que vous connaissiez les états de tous les attributs de votre objet suite à sa construction.

Visez une instanciation tardive : ne construisez un objet que quand vous serez en mesure de le faire de manière pleine et entière. Évitez ceci, qui est inefficace :

#include <ofstream>
#include <string>
#include <vector>
//
// ... using etc.
//
void ecrire_texte(const string &nom_fich, const vector<string> &lignes) {
   ofstream fich;       // <-- instanciation incomplète (objet pas encore vraiment utilisable; génération de code inutile)
   fich.open(nom_fich); // <-- complétion de l'initialisation (mauvaise idée)
   for(auto & s : lignes)
      fich << s << endl;
}

... et préférez plutôt ceci :

#include <ofstream>
#include <string>
#include <vector>
//
// ... using etc.
//
void ecrire_texte(const string &nom_fich, const vector<string> &lignes) {
   ofstream fich{nom_fich}; // <-- bien mieux! Seul le code essentiel est généré
   for(auto & s : lignes)
      fich << s << endl;
}

Si vous devez procéder à une initialisation en deux temps (construction puis initialisation), ce qui arrive, alors envisagez une fabrique.

Comprendre ses constructeurs

Avant d'utiliser une classe, mieux vaut jeter un coup d'oeil aux constructeurs qu'elle expose pour en tirer pleinement profit., Par exemple, au lieu d'écrire du code comme le suivant pour créer une chaîne contenant n fois le symbole:

#include <string>
//
// ... using etc.
//
string creer_ligne(int n, char c) {
   string s;
   for(int i = 0; i < n; ++i)
      s += c;
   return s;
}

... mieux vaut solliciter le constructeur approprié qui fera le travail de manière optimale :

#include <string>
//
// ... using etc.
//
string creer_ligne(int n, char c) {
   return string(n, c);
}

De même, comprendre le comportement d'un constructeur permet de simplifier l'écriture du code et de réduire les risques de confusion. Par exemple, devant l'extrait de code suivant :

#include <fstream>
//
// ... using etc.
//
int main() {
   ifstream in{ "in.txt", ios::in };
   // ...
}

... une programmeuse ou un programmeur serait en droit de se questionner sur la répétition des mentions à l'effet que in soit un flux en entrée (type std::ifstream) et que le comportement soit... d'être un flux en entrée (paramètre std::ios::in). Avec raison, d'ailleurs, car il serait plus simple et plus clair d'écrire ceci :

#include <fstream>
//
// ... using etc.
//
int main() {
   ifstream in{ "in.txt" };
   // ...
}

... qui ne fait dépendre le comportement que du type, sans risque d'erreur ou de confusion.

Quand construire

Si vous avez une répétitive comme celle-ci :

#include <string>
#include <fstream>
#include <vector>
//
// ... using etc.
//
void traiter_chaines(const vector<string> &v) {
   ifstream in;
   ofstream out;
   string s;
   for(const auto &chaine : v) {
      in = ifstream{chaine + ".txt"};
      out = ofstream{chaine + ".out.txt"};
      while (getline(in, s)) {
         // traiter s
         out << s << '\n';
      }
      in.close();
      out.close();
   }
}

... le traitement très local accordé aux variables in, out et s est un signe que ces objets ne sont pas construits au bon endroit, et devraient être intégrés au corps de la répétitive. Une écriture plus adéquate serait :

#include <string>
#include <fstream>
#include <vector>
//
// ... using etc.
//
void traiter_chaines(const vector<string> &v) {
   for(const auto &chaine : v) {
      ifstream in{chaine + ".txt"};
      ofstream out{chaine + ".out.txt"};
      for(string s; getline(in, s); ) {
         // traiter s
         out << s << '\n';
      }
   }
}

Remarquez au passage que cela allège le code, clarifie le fait qu'il soit correct d'enlever des artéfacts non-idiomatiques de C++ (appels explicites à close(), alors que les flux ont des destructeurs pour cela), et réduit la portée des variables au minimum (ici, s est même contraint à la répétitive for qui y a recours).

Dans le cas où une fonction contiendrait un objet const et coûteux à construire, par exemple ceci :

#include <string>
#include <regex>
//
// ... using etc.
//
string colorier_mots_cles(string s) {
   const string MOTS_CLES = "alignas alignof and and_eq asm auto bitand bitor bool break case catch char char16_t char32_t class compl concept const constexpr const_cast continue decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public register reinterpret_cast requires return short signed sizeof static static_assert static_cast struct switch template this thread_local throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while xor xor_eq"
   // faire plein de modifications dans s ...
   return s;
}

... mieux vaut être conscient(e) que la constante MOTS_CLES sera construite à chaque appel de colorier_mots_cles(). Dans un tel cas, il peut être préférable de la qualifier static pour qu'elle ne soit construite qu'au tout premier appel :

#include <string>
#include <regex>
//
// ... using etc.
//
string colorier_mots_cles(string s) {
   static const string MOTS_CLES = "alignas alignof and and_eq asm auto bitand bitor bool break case catch char char16_t char32_t class compl concept const constexpr const_cast continue decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public register reinterpret_cast requires return short signed sizeof static static_assert static_cast struct switch template this thread_local throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while xor xor_eq"
   // faire plein de modifications dans s ...
   return s;
}

Rôle du destructeur (C++)

Le destructeur d'un objet bien écrit assure le bon déroulement de sa finalisation. La présence de finalisation déterministe en C++ permet de mettre en application l'idiome RAII. Ceci permet d'alléger l'écriture du code et de le solidifier. Ainsi, plutôt que :

#include <ofstream>
#include <string>
#include <vector>
//
// ... using etc.
//
void utiliser(ostream&, const vector<string>&); // <-- que fait ceci? Mystère...
void ecrire_texte(const string &nom_fich, const vector<string> &lignes) {
   ofstream fich{nom_fich};
   utiliser(fich, lignes);
   fich.close(); // <-- pourquoi ne pas laisser fich faire son travail?
}

... préférez plutôt ceci :

#include <ofstream>
#include <string>
#include <vector>
//
// ... using etc.
//
void utiliser(ostream&, const vector<string>&); // <-- que fait ceci? Mystère...
void ecrire_texte(const string &nom_fich, const vector<string> &lignes) {
   ofstream fich{nom_fich, lignes};
   utiliser(fich);
}

... ou encore ceci, qui est encore mieux :

#include <ofstream>
#include <string>
#include <vector>
//
// ... using etc.
//
void utiliser(ostream&, const vector<string>&); // <-- que fait ceci? Mystère...
void ecrire_texte(const string &nom_fich, const vector<string> &lignes) {
   utiliser(ofstream(nom_fich), lignes);
}

Attributs dont le type est une référence (C++)

Une référence doit être liée à son référé dès son initialisation. Pour cette raison, si un objet a un attribut dont le type est une référence sur quelque chose, vous devez l'initialiser dès sa propre construction :

#include <ostream>
class Afficheur {
   std::ostream &os_; // <-- référence sur un flux
public:
   Afficheur(std::ostream &os) // <-- le flux sur lequel écrire
      : os_{os} // <-- c'est ici qu'il faut initialiser os_!
   {
      // ... car ici, il serait trop tard, la référence ayant été créée
   }
   // ...
};

Encapsulation et invariants

Si votre code est correct, alors un objet construit est présumé valide et respectueux de ses invariants. C'est la magie de l'approche OO. en particulier, portez attention aux opérations suivantes :

Qualifications const et volatile

Truc simple : si vous pouvez utiliser const, alors vous devriez probablement le faire. Soyez const-correct.

Soyez repectueux de ces qualifications :

Chaque fois que vous annotez correctement vos méthodes, vous vous aidez à éviter de faire des bêtises, et vous aidez le compilateur à produire du meilleur code pour vous.

Utiliser at() ou operator[] (C++)

Certains conteneurs (p. ex. : vector) offrent un opérateur [] et une méthode at() pour accéder à un élément par son indice. La différence entre les deux est que at() valide les bornes et lève une exception lors d'un débordement. Vous payez donc pour un test à chaque accès à un élément de votre conteneur. C'est bien trop cher payé; en pratique, validez votre code de manière rigoureuse a priori, et préférez operator[]... sauf si vous avez vraiment une excellente raison.

Types internes et publics (C++)

Ne présumez pas que les indices ou la taille de vos conteneurs sont des int. Les conteneurs documentent leurs types à même leurs interfaces. Le type des indices d'une string est string::size_type. Le type de la taille d'un vector<double> est vector<double>::size_type. Ne présumez pas de ce à quoi équivalent ces types, car les associations sont non-portables (ce qu'on compilateur peut asssocier à un unsigned int, un autre peut en faire un std::size_t ou un unsigned long) : ces types internes et publics sont documentés, ils sont là, utilisez-les.

Nous ne sommes pas tous américain(e)s (internationalisation, ou i18n)

Certaines fonctions de C, qu'on trouve typiquement dans <cctype>, sont écrites pour n'opérer que sur l'alphabet américain. Pensez à toupper(), tolower(), isalpha(), etc. Ces fonctions sont typiquement unaires et retournent soit un char, soit un bool.

Pour chacune de ces fonctions, il existe un équivalent qui tient compte de la culture de référence. La culture est représentée par une instance de std::locale (en-tête <locale>), où :

Ainsi, plutôt que ceci :

#include <cctype>
#include <string>
#include <algorithm>
//
// using...
//
bool exempt_de_blancs(const string &s) {
   return find_if(begin(s), end(s), [](char c) { return isspace(c); }) == end(s);
}
string majuscules(string s) {
   transform(begin(s), end(s), begin(s), [](char c) { return toupper(c); });
   return s;
}

... préférez ceci :

#include <locale>
#include <string>
#include <algorithm>
//
// using...
//
bool exempt_de_blancs(const string &s) {
   auto loc = locale{""};
   return find_if(begin(s), end(s), [&loc](char c) { return isspace(c, loc); }) == end(s);
}
string majuscules(string s) {
   auto loc = locale{""};
   transform(begin(s), end(s), begin(s), [&loc](char c) { return toupper(c, loc); });
   return s;
}

Il y a un coût à cette approche, soit celui de la construction d'un locale (c'est dispendieux), alors visez à créer le locale une fois puis à l'utiliser plusieurs fois.

Accéder au dernier élément d'un conteneur

Si vous ressentez la tentation d'écrire un truc comme ceci :

#include <vector>
#include <cassert>
//
// using...
//
template <class T>
   T dernier_element(const vector<T> &v) {
      assert(!v.empty());
      return v[v.size()-1];
   }

... retenez-vous. Le recours aux parenthèses pour créer une variable temporaire est subtile, et il est probable que ce soit un chemin non-optimal pour en arriver au résultat souhaité.

Sachez que les conteneurs offrent pour la plupart un service back() qui fait exactement ce travail, mais le fait correctement :

#include <vector>
#include <cassert>
//
// using...
//
template <class T>
   T dernier_element(const vector<T> &v) {
      assert(!v.empty());
      return v.back();
   }

Notez que back() retourne une référence (const ou non, selon le contexte) sur le dernier élément, ce qui ajoute à sa flexibilité.

De même, si vous avez envie d'écrire ceci :

#include <vector>
#include <cassert>
//
// using...
//
template <class T>
   vector<T>::iterator iterateur_sur_dernier_element(vector<T> &v) {
      assert(!v.empty());
      return --(v.end());
   }

... souvenez-vous qu'il est plus simple (et plus propre) d'utiliser l'algorithme prev() et d'écrire ceci :

#include <vector>
#include <algorithm>
#include <cassert>
//
// using...
//
template <class T>
   vector<T>::iterator iterateur_sur_dernier_element(vector<T> &v) {
      assert(!v.empty());
      return prev(v.end());
   }

Vous n'avez rien à perdre, tout à gagner.

Inclusions et fichiers d'en-tête

Les langages C et C++ utilisent un mode d'inclusion lexical, qui a des qualités (dont la simplicité et la facilité de séparer, dans la plupart des cas, l'interface de l'implémentation) et des défauts (irritants propres au modèle, beaucoup de travail redondant pour un compilateur). Plus de détails dans ../Developpement/Programmation-Systeme--Mecanique-Compilation.html.

La règle de base est : incluez ce dont vous avez besoin, pas plus, pas moins. Visez à rendre chaque fichier autonome, mais à ne pas imposer vos choix esthétiques à vos clients.

Par exemple, si vous déclarez la classe suivante :

class Personne {
public:
   using str_type = std::string;
private:
   str_type nom_;
   short age_;
public:
   Personne() = delete;
   Personne(const str_type&, short);
   str_type nom() const {
      return nom_;
   }
   short age() const {
      return age_;
   }
   friend bool operator==(const Personne&, const Personne&);
   friend bool operator!=(const Personne&, const Personne&);  
};
std::ostream& operator<<(std::ostream&, const Personne&);

... alors vous devez inclure <string>, du fait que Personne possède un attribut du type std::string et donc que la structure et la taille de std::string participent à celles de Personne, mais vous n'avez aucune raison d'inclure un monstre comme <iostream>, puisque nous n'utilisons pas les flux standards comme std::cin ou std::cout dans cette déclaration.

Vous devez introduire le nom std::ostream, bien sûr, mais cela peut être fait en n'incluant que <ostream>, ce qui est déjà plus léger, ou mieux encore, en incluant <iosfwd> qui est beaucoup plus léger et n'introduit que des noms de types, sans leur implémentation dont nous n'avons aucunement besoin ici.

Évidemment, en définissant l'opérateur de projection sur un flux pour Personne (probablement dans Personne.cpp), vous devrez inclure <ostream> (ou <iostream>, mais c'est probablement abusif), mais ce choix, en étant fait dans un .cpp plutôt que dans un .h, n'aura qu'un impact local.

Pour en savoir plus :

Texte de 2014 par Arseny Kapoulkine : http://zeuxcg.org/2010/11/15/include-rules/

Ordre des inclusions (C++)

Pour plus de détails, voir ordre_inclusions.html.

Si vous incluez des en-têtes maison et des en-têtes standards prenez l'habitude d'inclure d'abord les en-têtes maison. Ce conseil de John Lakos découle des constats suivants :

Exemple d'un bogue masqué :

a.h (incomplet)
#ifndef A_H
#define A_H
//
// Retourne une chaîne épurée de tout « blanc » (espace, tabulation, saut de ligne, etc.)
//
std::string epurer(const std::string&); // <-- oups! <string> n'a pas été inclus!
#endif
a.cpp (à ne pas faire)
#include <string> // <-- introduit std::string
#include <locale>
#include <algorithm>
#include <iterator>
#include "a.h" // <-- ici, std::string est connu à cause de l'inclusion de <string> plus haut
using namespace std;
string epurer(const string &src) {
   string resultat;
   auto loc = locale{""};
   copy_if(begin(src), end(src), back_inserter(resultat), [&loc](char c) {
      return !isspace(c, loc);
   });
   return resultat;
}

Ici, a.h est incorrect mais le code de a.cpp masque les manques de a.h. Si nous livrons a.h dans cet état, nous causerons des maux de tête au code client, et ce par pure négligence de notre part.

En changeant l'ordre des inclusions dans a.cpp, le bogue sera identifié par le compilateur :

a.cpp (correct; permet de détecter le bogue)
#include "a.h" // <-- ici, std::string est inconnu; la déclaration de epurer(const string&) ne compile pas
#include <string> // <-- introduit std::string
#include <locale>
#include <algorithm>
#include <iterator>
using namespace std;
string epurer(const string &src) {
   string resultat;
   auto loc = locale{""};
   copy_if(begin(src), end(src), back_inserter(resultat), [&loc](char c) {
      return !isspace(c, loc);
   });
   return resultat;
}

Une fois le bogue repéré, reste à corriger le fichier d'en-tête a.h :

a.h (correct)
#ifndef A_H
#define A_H
#include <string>
//
// Retourne une chaîne épurée de tout « blanc » (espace, tabulation, saut de ligne, etc.)
//
std::string epurer(const std::string&); // Ok
#endif

Terminer tout fichier d'en-tête par un saut de ligne (C++)

Fait peu connu, mais dans du code C++ standard, un en-tête doit se terminer par un saut de ligne. La raison est simple : l'inclusion des fichiers est faite de manière lexicale, et si la dernière ligne d'un .h n'est pas vide, alors cette ligne sera concaténée au début de la première ligne des fichiers qui l'incluront.

Certains compilateurs compensent pour cette « erreur » commune des programmeuses et des programmeurs en insérant un saut de ligne fantôme dans les fichiers .h lors de la compilation, mais il ne faut pas compter là-dessus en pratique.

Instructions using (C++)

Dans un fichier .h, évitez à tout prix d'utiliser des instructions using au niveau du fichier lui-même (surtout, évitez les using namespace!). La raison : les fichiers qui incluront votre .h seraient alors liés par vos choix. Si vous avez utilisé une instruction comme using std::string, par exemple, alors un fichier incluant votre .h mais souhaitant utiliser son propre type string se trouvera coincé avec votre choix de type, qui n'est pas adéquat pour lui.

Dans un fichier .h, par contre, vous pouvez utiliser des instructions using locales à des fonctions ou à des méthodes. Celles-ci n'engagent que vous et allègent votre code.

a.h (vilain) a.h (mieux) a.h (vive C++ 11) a.h (vive C++ 14)
#ifndef A_H
#define A_H
#include <string>
#include <locale>
#include <vector>
using namespace std; // <-- très vilain
string epurer(const string &);
class Epureur {
   const locale &loc;
public:
   Epureur(const locale &loc = locale("")) : loc(loc) {
   }
   vector<string> operator()(const vector<string> v) {
      using size_type = vector<string>::size_type;
      vector<string> resultat;
      for(size_type i = {}; i < v.size(); ++i) {
         string s = epurer(v[i]);
         if (!s.empty())
            resultat.push_back(s);
      }
      return resultat;
   }
};
#endif
#ifndef A_H
#define A_H
#include <string>
#include <locale>
#include <vector>
std::string epurer(const std::string &);
class Epureur {
   const std::locale &loc;
public:
   Epureur(const std::locale &loc = std::locale{""}) : loc(loc) {
   }
   std::vector<std::string> operator()(const std::vector<std::string> v) {
      using namespace std;
      using size_type = vector<string>::size_type;
      vector<string> resultat;
      for(size_type i = {}; i < v.size(); ++i) {
         string s = epurer(v[i]);
         if (!s.empty())
            resultat.push_back(s);
      }
      return resultat;
   }
};
#endif
#ifndef A_H
#define A_H
#include <string>
#include <locale>
#include <vector>
std::string epurer(const std::string &);
class Epureur {
   const std::locale &loc;
public:
   Epureur(const std::locale &loc = std::locale{""}) : loc{loc} {
   }
   std::vector<std::string> operator()(const std::vector<std::string> v) {
      vector<string> resultat;
      for(auto &str : v) {
         auto s = epurer(str);
         if (!s.empty())
            resultat.push_back(s);
      }
      return resultat;
   }
};
#endif
#ifndef A_H
#define A_H
#include <string>
#include <locale>
#include <vector>
std::string epurer(const std::string &);
class Epureur {
   const std::locale &loc;
public:
   Epureur(const std::locale &loc = std::locale{""}) : loc{loc} {
   }
   auto operator()(const std::vector<std::string> v) {
      vector<string> resultat;
      for(auto &str : v) {
         auto s = epurer(str);
         if (!s.empty())
            resultat.push_back(s);
      }
      return resultat;
   }
};
#endif

Dans un fichier .cpp, bien entendu, vous êtes libres.

Quelle version de l'en-tête? (C++)

Plusieurs en-têtes disponibles pour les programmes C++ viennent en deux « saveurs », par exemple <math.h> et <cmath> ou encore <time.h> et <ctime>. Ce que vous devez savoir :

Compilation séparée et One Definition Rule (ODR) (C++)

Pour en savoir plus, lisez ce texte sur la mécanique de compilation en C++.

C++, comme C, préconise la compilation séparée, et exige qu'il n'y ait qu'une seule définition pour chaque chose; un fichier source est en violation d'ODR s'il a plus d'une définition pour un même nom (et ce même si les deux définitions sont identiques). Par exemple, ci-dessous, le programme vilain.cpp est en violation de la règle ODR à cause de la double définition de la variable x et de la fonction f(). Notez que y ne pose pas de problème car les deux variables de ce nom ne sont pas définies au même niveau hiérarchique, ce qui évite les conflits de noms. Notez aussi que g() ne pose pas de problème à la compilation (l'appel respecte la signature annoncée par son prototype), mais il faudra qu'une définition (unique) soit rendue disponible au moment de l'édition des liens :

vilain.cpp
int x = 3,
    y = 4;
int f(int n) {
   return n + x;
}
int g(int);
#include <iostream>
int main() {
   using namespace std;
   int y = f(-2);
   cout << g(y) << endl;
}
float x = 3.14159f; // <-- violation d'ODR
int f(int n) { // <-- violation d'ODR
   return n * n;
}

Sachant de l'inclusion des fichiers d'en-têtes est lexicale, il faut comprendre ce qui peut être placé (ou non) dans un tel fichier pour éviter les erreurs. Les règles sont simples, au fond : un .h ne peut contenir que des déclarations ou des définitions inline, donc des éléments qui ne risquent pas de causer de violations d'ODR :

  • Des définitions de types (struct, class, enum, union, etc.) et des alias (typedef)
  • Des prototypes de fonctions globales, car ce ne sont que des déclarations, pas des définitions
  • Des déclarations de variables (extern), sans définitions (il faut alors que la définition soit unique et placée dans un .cpp qui sera lié au moment de l'édition des liens)
  • Des définitions de variables globales qualifiées static, ou encore placées dans un namespace anonyme (ces définitions n'apparaissent pas à l'édition des liens)
  • Des définitions de fonctions globales inline
  • Des définitions de méthodes à même la déclaration de leur classe. Pour le compilateur, ceci signifie que la méthode doit être considérée inline

Note importante : tout template doit être défini à même le .h qui le déclare, car le code effectif du template est généré par le compilateur au point d'utilisation et doit, pour cette raison, être visible au compilateur à ce moment.

#ifndef A_H
#define A_H
//
// Ce qui suit est légal dans un .h
//
struct Point {
   int x, y, z;
   Point(int x, int y, int z) : x{x}, y{y}, z{z} {
   }
   bool operator==(const Point &pt) const {
      return x == pt.x && y == pt.y && z == pt.z;
   }
   friend bool operator==(const Point &, const Point&);
   // ...
};
class Surprise; // déclaration a priori
Surprise cadeau(); // Ok
class Trois {
public:
   bool est_impair() const {
      return true;
   }
   static int valeur() {
      return 3;
   }
   bool est_premier() const;
};
//
// doit être définie dans un .cpp accessible à l'édition des liens
//
extern const float PI;
inline long carre(int n) {
   return n * n;
}
template <class T>
   bool est_pair(const T& val) {
      return val % 2 == 0;
   }
template <>
   bool est_pair(Trois) {
      return false;
   }
template <int N, class T>
   class Tableau {
   public:
      using value_type = T;
      using size_type = int;
   private:
      value_type elems[N];
   public:
      size_type size() const {
         return N;
      }
      value_type& operator[](size_type n) {
         assert(0 <= n && n < size());
         return elems[n];
      }
      value_type operator[](size_type n) const {
         assert(0 <= n && n < size());
         return elems[n];
      }
   };
//...

Il ne faut donc pas mettre dans un .h les choses suivantes :

  • Définition de fonction globale
  • Définition de méthode hors de la déclaration de sa classe
  • Définition de variable globale
  • Définition de constante, outre les constantes entières (qui ont un statut privilégié)

Portez attention à ce texte de Bruce Dawson, portant entre autres sur le coût de certaines pratiques (en apparence anodines) quant au lieu où nous définissons des constantes : http://randomascii.wordpress.com/2014/06/26/please-calculate-this-circles-circumference/

En 2016, Andrzej Krzemieński rapporte un vilain problème résultant d'une violation d'ODR et propose une approche pour l'éviter : https://akrzemi1.wordpress.com/2016/11/28/the-one-definition-rule/

// ...
//
// Ce qui suit risque de provoquer une violation d'ODR
//
int init = 0;
int cube(int n) {
   return n * carre(n);
}
bool operator!=(const Point &p0, const Point &p1) {
   return ! (p0 == p1);
}
bool Trois::est_premier() const {
   return true;
}
#endif

Évitez les invariants cachés

Si l'une de vos classes comporte des invariants cachés, il se peut que ce soit une mauvaise idée... Et il est presque certain que vous devrez tenir compte de la Sainte-Trinité.

Par invariant caché, on entend une situation qui doit s'avérer mais qui n'est pas explicite de par l'interface de la classe. Un exemple type serait celui d'une classe contenant entre autres deux états, soit un conteneur et un pointeur sur l'un de ses éléments, disons celui de plus haute valeur pour que l'exemple soit plus concret (merci à Michael Caisse pour l'idée) :

// ...
#include <vector>
class ConteneurSubtil {
   std::vector<int> v;
   int *p;
public:
   template <class It>
      ConteneurSubtil(It debut, It fin) : v(debut, fin) {
         p = std::max_element(std::begin(v), std::end(v));
      }
   // ...
};

Si, pour une instance de la classe ConteneurSubtil, il est important que p pointe toujours sur l'élément de v qui a la plus haute valeur, alors on ne peut laisser la Sainte-Trinité être générée implicitement. En effet, imaginons ce code de test :

// ...
int main() {
   int vals [] = { 2,3,5,7,11 };
   ConteneurSubtil cs0{begin(vals), end(vals)}; // copie vals dans cs0.v; cs0.p pointe sur &cs0.v[4]
   ConteneurSubtil cs1 = cs0; // copie cs0 dans cs1; cs1.v est une copie de cs0.v, mais cs1.p pointe sur... &cs0.v[4]!
};

On peut imaginer les comséquences par la suite : si ConteneurSubtil est en fait une sorte de file prioritaire, et si son p y est le prochain élément à extraire, alors retirer le « prochain » élément de cs1 modifierait cs0 à son insu. Ceci risque d'être quelque peu périlleux.

Les invariants cachés forcent donc souvent la prise en charge explicite de la Sainte-Trinité et de la sémantique de mouvement. Mieux vaut les implémenter avec parcimonie et les traiter avec soin quand nous avons recours à eux.

Pratique de la programmation

Quelques conseils généraux de programmation, principalement avec C++.

Utiliser des boucles « manuelles » ou utiliser des algorithmes?

Préférez les algorithmes, même pour des opérations aussi simples que trouver la plus petite de deux valeurs ou permuter deux valeurs. Par exemple :

Plutôt que ceci...... préférez cela
#include <string>
// ... using ...
string::size_type longueur_min(const string &s0, const string &s1) {
   string::size_type resultat = {};
   if (s0.size() < s1.size())
      resultat = s0.size();
   else
      resultat = s1.size();
   return resultat;
}
// ...
#include <algorithm>
#include <string>
// ... using ...
string::size_type longueur_min(const string &s0, const string &s1) {
   return max(s0.size(), sq.size());
}
// ...
#include <string>
#include <vector>
// ... using ...
string concatener(const vector<string> &v) {
   if (v.empty()) return {};
   string resultat = v.front();
   for(auto p = next(begin(v)); p != end(v); ++p)
      resultat += "\n" + *p;
   return resultat;
}
// ...
#include <numeric>
#include <string>
#include <vector>
// ... using ...
string concatener(const vector<string> &v) {
   return v.empty()? {} : accumulate(next(begin(v)), end(v), v.front(), [](const string &s0, const string &s1) {
      return s0 + "\n" + s1;
   });;
}
// ...

Ne réécrivez pas le code écrit, testé et optimisé. Appliquez votre énergie et votre créativité aux problèmes qui ont besoin de solutions.

Variables globales : pour ou contre?

Contre. Truc simple : dites non aux variables globales, à moins d'une excellente raison. C'est un panier de crabes dont vous n'avez pas besoin; avec elles, votre code sera plus fragile et plus lent, et la situation sera exacerbée si vous avez plus d'un thread par programme. Appliquez le principe de localité, c'est à votre avantage.

Nommer une variable?

Dû à plusieurs optimisations telles que RVO (le Return Value Optimization) et la sémantique de mouvement, il est souvent avantageux de ne pas nommer une variable qui ne joue un rôle que localement à une expression. Par exemple :

Bien Mieux (se prête à optimisation)
vector<string> generer() {
   vector<string> v;
   v.push_back("J'aime");
   v.push_back("mon");
   v.push_back("prof");
   return v;
}
vector<string> generer() {
   return { "J'aime", "mon", "prof" };
}
entier addition(const entier &a, const entier &b) {
   entier resultat = a + b;
   return resultat;
}
entier addition(const entier &a, const entier &b) {
   return a + b;
}

L'idée générale est que le compilateur, en constatant qu'une variable est anonyme, sait que le programme ne pourra pas y référer ultérieurement et peut se donner à fond dans la gamme possible d'optimisations sur elle.

Notez que le recours à des variables anonymes peut se faire en appelant les constructeurs avec des parenthèses ou avec des accolades, mais les accolades évitent un effet grammatical déplaisant connu sous le nom de C++'s Most Vexing Parse. Par exemple, dans le code suivant :

string f(string);
void g() {
   f(string()); // ceci semble propre, mais il y a un piège grammatical
}

... la notation string() apparaît pour le compilateur comme une signature de fonction ne prenant pas de paramètre et retournant une string. Pour s'assurer d'avoir une string par défaut, il faut remplacer l'expression string() par (string()) où les parenthèses englobantes forcent l'évaluation de l'expression. Par contre, dans le code suivant, le remplacement des parenthèses pas des accolades élimine complètement l'ambiguïté :

string f(string);
void g() {
   f(string{}); // ouf, ceci est clair...
}

Références ou pointeurs?

Truc simple : référence si possible, pointeur si nécessaire. Et dans l'immense majorité des cas, privilégiez les pointeurs intelligents aux pointeurs bruts.

Passer un paramètre par valeur, par référence ou par référence-sur-const?

Quelques trucs simples...

Considérez le passage par mouvement comme une optimisation potentielle quand il est propice de l'offrir.

Préférer NULL, 0 ou nullptr?

Truc simple : préférez nullptr.

Si votre compilateur n'est pas à jour, préférez 0 à NULL, qui est une macro. Voir ../Divers--cplusplus/CPP--NULL.html pour plus de détails.

Construire une string vide

Supposons que vous souhaitiez avoir une chaîne de caractères s qui soit initialement vide. Si vous avez le choix entre les six écritures suivantes :

string s;
string s = "";
string s = string();
string s {};
string s = {};
string s();

... alors vos meilleures options avec C++ 11 sont les options 4 et 5, qui sont équivalentes et sans ambiguïté, suivies par la 3, qui est équivalente à la 1 mais qui est plus flexible.  La 2 manque de généralité. La 6 n'est pas une string mais bien un prototype d'une fonction nommée s ne prenant pas de paramètre et retournant une string. Pour plus de détails, examinez ce qui suit :

Avec une classe Avec un primitif Version générique
string s;
string s = "";
string s = string();
string s {};
string s = {};
string s();
int i;
int i = 0;
int i = int();
int i {};
int i = {};
int i();
T val; // ?
T val = 0; // ?
T val = T();
T val {};
T val = {};
T val();

En généralisant le code, on peut voir que la 1 fonctionne bien pour un objet muni d'un constructeur par défaut mais laisse un primitif non-initialisé. La 2, qui initialise à partir d'un littéral, n'a de sens que dans des cas particuliers et n'est pas généralisable. La 3 force une initialisation à la valeur par défaut du type, qui est le zéro du type pour les primitifs. La 4 et la 5 font de même, mais sont plus légères à rédiger et plus homogènes. Dans tous les cas, la 6 est un prototype de fonction.

Vider une string

Si vous avez le choix entre les deux écritures suivantes :

s = "";
s.clear();

... alors privilégiez la seconde, qui explicite votre intention. La première est une affectation d'un const char* à une std::string, et le compilateur doit générer une répétitive pour compter les caractères du const char* source, allouer de l'espace en conséquence dans la std::string de destination, puis copier les caractères de la source vers la destination. Dans la deuxième, il peut simplement informer la std::string de destination que le nombre de caractères qu'elle contient est désormais nul.

Vérifier si un conteneur est vide

Si vous avez le choix entre les deux écritures suivantes pour vérifier si une string est vide :

if (s == "") { /* ... */ }
if (s.empty()) { /* ... */ }

... alors privilégiez la seconde, qui explicite votre intention. La première compare un const char* avec une std::string, et le compilateur doit générer une répétitive pour comparer les caractères un à un, alors que la seconde peut se limiter à tester le nombre de caractères dans s, une opération en temps constant.

En général, si vous avez le choix entre les deux écritures suivantes pour un conteneur v donné :

if (v.size() == 0) { /* ... */ }
if (v.empty()) { /* ... */ }

... alors privilégiez la seconde. La première sersa équivalente seulement si la méthode size() peut être évaluée en temps constant (complexité ), or ce n'est absolument pas garanti : pour certains conteneurs, par exemple les listes simplement chaînées, tenir à jour un attribut représentant le nombre d'éléments de la liste est typiquement superflu, acroissant la taille du conteneur sans que ce service ne soit pertinent pour les opérations normales. Dans un tel cas, évaluer size() impliquera compter les noeuds de la liste, un à un, une opération de complexité linéaire (complexité ).

Connaître la taille d'un tableau

Voir Deduire-taille-tableau-genericite.html

Opérateurs

Certaines saines pratiques tiennent au bon usage des opérateurs.

Opérateur ternaire (le ?:)

L'opérateur ternaire peut remplacer une séquence complexe d'instructions comme celle de gauche, ci-dessous, par une seule et même expression comme celle du centre ou, mieux encore, comme celle de droite.

string message(bool succes) {
   string resultat;
   if (succes)
      resultat = "Bravo!";
   else
      resultat = "Zut!";
   return resultat;
}
string message(bool succes) {
   string resultat = succes? "Bravo!" : "Zut!";
   return resultat;
}
string message(bool succes) {
   return succes? "Bravo!" : "Zut!";
}

La version de gauche requiert une variable temporaire, initialisée (inutilement) à sa valeur par défaut, puis dont le contenu est remplacé par autre chose ultérieurement. Il y a beaucoup de gaspillage dans ces quelques lignes.

La version du centre est meilleure, mais en utilisant une variable intermédiaire, nous envoyons un faux signal au compilateur. En effet, quand nous nommons quelque chose dans un programme, cela indique généralement au compilateur que l'entité ainsi nommée a un rôle à jouer. Cela peut être utile dans le cas d'une variable temporaire qui éviterait de refaire des calculs redondants, mais dans ce cas, la variable n'est en fait qu'un commentaire réifié.

Conséquemment, la version de droite est, ici, la meilleure des trois avenues.

Notez au passage que l'expression c?a:b a un sens relativement pointu :

Les expressions a et b doivent être de même type, ou du moins avoir un type commun. Le type de l'expression c?a:b est donc typename std::common_type<a,b>::type.

Notez que quand l'objectif d'une expression reposant sur cet opérateur est de réaliser une affectation, il est mal vu d'utiliser la formule à gauche ci-dessous. En effet, cette forme exige de déclarer la variable d'abord, puis de l'initialiser ensuite, ce qui est inefficace (et inélégant). En retour, la forme à droite est à privilégier, étant plus compacte, plus directe et évitant des étapes intermédiaires (incluant des initialisations bidons).

Ne faites pas ceci (pas joli) Faites plutôt cela (beaucoup mieux)
vector<string> f();
vector<string> g();
vector<string fct(int n) {
   vector<string> v; // construction par copie (inutile)
   // remplacement de v... On utilise, à tort, :? comme un if
   n < 0? v = f() : v = g();
   // utiliser v...
   return v;
}
vector<string> f();
vector<string> g();
vector<string fct(int n) {
   auto v = n < 0? f() : g(); // initialisation direct. Bref, efficace
   // utiliser v...
   return v;
}

Opérateur d'auto-incrémentation

Quand une expression complexe inclut une auto-incrémentation, qu'il s'agisse de ++i ou de i++, la chose à faire est d'utiliser l'opérateur qui offre la sémantique attendue, soit i++ si la valeur de i avant incrémentation est celle souhaitée ou ++i si c'est la valeur après incrémentation qui compte.

Lorsque pris isolément, préférez la version préfixée, qui évite la création d'une variable temporaire. Une exception : vous écrivez du code parallèle, où i est un int... Dans ce cas, le parallélisme interne à votre processeur pourrait bénéficier de la version i++.

Dans l'acception abstraite, préférez ++i. En général, dans le doute, mesurez!


Valid XHTML 1.0 Transitional

CSS Valide !