Cette section contient des règles et directives qui sont populaires quelque part, mais que nous ne recommandons délibérément pas.
Nous savons parfaitement qu’il existe des périodes et des contextes où ces règles avaient du sens, et nous les avons utilisées nous‑mêmes à certains moments.
Cependant, dans le cadre des styles de programmation que nous recommandons et soutenons par le biais de ces lignes directrices, ces « non‑règles » sont préjudiciables.
Aujourd’hui encore, il existe des contextes où les règles peuvent avoir du sens.
Par exemple, l’absence de support d’outils adaptés peut rendre les exceptions inadaptées dans les systèmes temps réel stricts, mais ne faites pas naïvement confiance à la « sagesse populaire » (par ex. des affirmations non étayées concernant l’« efficacité »).
Cette « sagesse » peut être fondée sur des informations d’antan ou des expériences issues de langages très différents du C++ (par exemple, C ou Java).
Les arguments positifs proposant des alternatives à ces non‑règles sont listés dans les règles proposées comme « Alternatives ».
return dans une fonctiongoto exitprotectedLa règle « toutes les déclarations en haut » provient de vieux langages de programmation qui ne permettaient pas l’initialisation de variables et de constantes après une instruction.
Cela conduit à des programmes plus longs et à davantage d’erreurs dues à des variables non initialisées ou mal initialisées.
int use(int x)
{
int i;
char c;
double d;
// ... du code ...
if (x < i) {
// ...
i = f(i, d);
}
if (i < x) {
// ...
i = g(i, c);
}
return i;
}
Plus la distance entre une variable non initialisée et son utilisation est grande, plus le risque d’erreur augmente.
Heureusement, les compilateurs détectent de nombreuses erreurs « utilisé avant d’être configuré ».
Malheureusement, ils ne peuvent pas détecter toutes les erreurs et, de plus, les bogues ne sont pas toujours aussi simples à repérer que dans cet exemple.
return dans une fonctionLa règle d’unicité du retour peut conduire à un code inutilement complexe et à l’introduction de variables d’état supplémentaires.
En particulier, elle rend plus difficile la concentration de la vérification d’erreurs en haut de la fonction.
template<class T>
// nécessite Number<T>
string sign(T x)
{
if (x < 0)
return "negative";
if (x > 0)
return "positive";
return "zero";
}
Pour n’utiliser qu’un seul retour, il faudrait faire quelque chose comme
template<class T>
// nécessite Number<T>
string sign(T x) // mauvais
{
string res;
if (x < 0)
res = "negative";
else if (x > 0)
res = "positive";
else
res = "zero";
return res;
}
Ceci est plus long et probablement moins efficace.
Plus la fonction est grande et complexe, plus les solutions de contournement deviennent pénibles.
Bien sûr, de nombreuses fonctions simples n’auront naturellement qu’un seul return à cause de leur logique plus simple.
int index(const char* p)
{
if (!p) return -1; // indicateur d’erreur : alternativement « throw nullptr_error{} »
// ... faire une recherche pour trouver l’index de p
return i;
}
Si l’on appliquait la règle, on obtiendrait quelque chose comme
int index2(const char* p)
{
int i;
if (!p)
i = -1; // indicateur d’erreur
else {
// ... faire une recherche pour trouver l’index de p
}
return i;
}
Notez que nous (délibérément) avons violé la règle contre les variables non initialisées car ce style conduit généralement à cela.
De plus, ce style est une tentation d’utiliser le non‑règle goto exit.
return (et à lancer des exceptions).Il semble y avoir quatre raisons principales pour ne pas utiliser d’exceptions :
Il n’existe aucun moyen de satisfaire tout le monde sur ce sujet.
Après tout, les discussions sur les exceptions se poursuivent depuis plus de quarante ans.
Certains langages ne peuvent pas être utilisés sans exceptions, d’autres ne les prennent pas en charge.
Cela crée des traditions fortes pour l’usage ou l’abstention des exceptions, ainsi que des débats houleux.
Cependant, nous pouvons énumérer brièvement pourquoi nous considérons les exceptions comme la meilleure alternative pour la programmation générale et dans le contexte de ces lignes directrices.
Les arguments simples pour et contre sont souvent inconclusifs.
Il existe des applications spécialisées où les exceptions peuvent effectivement être inappropriées (par ex. les systèmes temps réel stricts sans prise en charge fiable des estimations du coût d’une gestion d’une exception).
Examinons les objections majeures aux exceptions, une par une :
Beaucoup, voire la plupart, des problèmes liés aux exceptions proviennent des besoins historiques d’interagir avec un ancien code encombré.
Les arguments fondamentaux en faveur de l’utilisation des exceptions sont :
Souvenez‑vous :
???
Expects et Ensures (jusqu’à ce que le langage prenne en charge les contrats)Le nombre de fichiers résultant de la séparation de chaque classe dans son propre fichier est difficile à gérer et peut ralentir la compilation.
Les classes individuelles ne constituent rarement une unité logique de maintenance et de distribution efficace.
???
Diviser l’initialisation en deux phases entraîne des invariants plus faibles, du code plus compliqué (doit gérer des objets partiellement construits) et des erreurs (lorsqu’on ne traite pas correctement les objets partiellement construits de façon cohérente).
On l’appelle parfois également construction en deux étapes.
// Ancien style conventionnel : nombreux problèmes
class Picture
{
int mx;
int my;
int * data;
public:
// Problème principal : le constructeur ne construit pas entièrement
Picture(int x, int y)
{
mx = x; // Mauvais : assignation dans le corps du constructeur
// plutôt que dans l’initialiseur de membre
my = y;
data = nullptr; // Mauvais : initialisation constante dans le constructeur
// plutôt que dans l’initialiseur de membre
}
~Picture()
{
Cleanup();
}
// ...
// Mauvaise : initialisation en deux phases
bool Init()
{
// Vérification d’invariant
if (mx <= 0 || my <= 0) {
return false;
}
if (data) {
return false;
}
data = (int*) malloc(mx*my*sizeof(int)); // Mauvais : pointeur brut et malloc propriétaire
return data != nullptr;
}
// Mauvais : aucune raison de séparer le nettoyage dans une fonction distincte
void Cleanup()
{
if (data) free(data);
data = nullptr;
}
};
Picture picture(100, 0); // pas encore prêt à l’emploi
// cela échouera…
if (!picture.Init()) {
puts("Error, invalid picture");
}
// on possède maintenant un objet Picture invalide.
class Picture
{
int mx;
int my;
vector<int> data;
static int check_size(int size)
{
// vérification d'invariant
Expects(size > 0);
return size;
}
public:
// mieux serait une classe pour une taille 2D comme un seul paramètre
Picture(int x, int y)
: mx(check_size(x))
, my(check_size(y))
// nous savons maintenant que x et y ont une taille valide
, data(mx * my) // lèvera std::bad_alloc en cas d'erreur
{
// l’image est prête à l'emploi
}
// le destructor généré par le compilateur fait le travail (voir aussi C.21)
// ...
};
Picture picture1(100, 100);
// l'image picture1 est prête à l'emploi ici...
// ce n'est pas une taille valide pour y,
// le comportement par défaut en cas de violation de contrat appellera std::terminate alors
Picture picture2(100, 0);
// ne sera pas atteint ici...
goto exitgoto est source d’erreurs.
Cette technique constitue une approche « pré‑exceptions » pour la gestion des ressources et des erreurs similaire à RAII.
void do_something(int n)
{
if (n < 100) goto exit;
// ...
int* p = (int*) malloc(n);
// ...
if (some_error) goto exit;
// ...
exit:
free(p);
}
et repérez le bogue.
protectedLes données protected sont une source d’erreurs.
Les données protected peuvent être manipulées depuis un nombre illimité de code dans divers endroits.
Les données protected sont l’équivalent de l’héritage hiérarchique des données globales.
???
protected @TODO-LINK