Quelques raccourcis :
Il est possible d'écrire des horreurs même en s'en tenant aux bases de la programmation...
La situation : supposons qu'on veuille écrire une fonction est_pair() qui prenne en paramètre un entier et qui retourne la valeur booléenne vrai (true) si cet entier est pair, et faux (false) s'il est impair.
L'horreur : les cas considérés ici ne sont pas des horreurs au sens strict du terme. Il s'agit dans chaque cas de solutions opérationnelles mais déficientes en forme ou en style (qui ne sont pas non plus des canons d'élégance). Voyons une première version :
bool est_pair(int nb) {
if (nb % 2 == 0) { // si nb est pair
return true;
} else {
return false;
}
}
Cette fonction est opérationnelle. Elle répond aux exigences de l'énoncé du problème, mais ne répond pas aux critères de bonne programmation en vigueur au département. Il s'y trouve beaucoup de redondance, à laquelle nous reviendrons sous peu, mais elle offre surtout deux points de sortie, ce qui rend impossible la tâche d'en réaliser un morphogramme.
Ne pas pouvoir présenter un morphogramme pour un algorithme de base empêche de l'enboîter structurellement dans un schéma plus complexe et complique une éventuelle démonstration de bon fonctionnement pour un programme se servant de cette fonction.
Voyons maintenant une deuxième version, qui corrige cette faute structurelle et utilise une variable temporaire pour éliminer la dualité des points de sortie :
bool est_pair(int nb) {
bool resultat;
if (nb % 2 == 0) { // si nb est pair
resultat = true;
} else {
resultat = false;
}
return resultat;
}
Cette version est structurellement correcte, mais est lente et complexe pour le problème qu'elle a à résoudre. En effet, si on la lit en langage naturel, on a :
Ce qui est équivalent, soit dit en passant, à :
La forme alternative ici entraîne une redondance qui devient peut-être visible à l'oeil étant donné la mise en relief qui précède. Nous exploiterons l'information dans cette deuxième forme pour déduire une solution optimale au problème, un peu plus loin.
Pour tenter de simplifier cette solution, on serait tenté d'exploiter une version de la fonction qui initialise la variable resultat avec une valeur par défaut, comme par exemple :
bool est_pair(int nb) {
bool resultat = false;
if (nb % 2 == 0) { // si nb est pair
resultat = true;
}
return resultat;
}
Cette version est plus compacte que la précédente, mais elle est aussi au moins aussi lente, étant même plus lente dans le cas où nb est effectivement pair. La raison :
La forme plus compacte dans ce cas-ci n'est pas celle qui fait le moins de choses.
Le bon usage : reconnaître que le résultat de l'évaluation de la condition est un booléen, et qu'on peut l'utiliser de la même manière qu'on utiliserait un littéral booléen. Ainsi, constatant que si la condition est vraie, alors on veut retourner vrai, et que si la condition est fausse, alors on veut retourner faux, on réalisera que dans le fond, notre but est de retourner la valeur résultant de l'évaluation de la condition :
bool est_pair(int nb) {
return nb % 2 == 0;
}
Selon l'ordre de priorité des opérateurs, la calcul du reste de la division de nb par 2 se fera d'abord, résultant en un entier compris inclusivement entre 0 et 1, et la comparaison entre cet entier et le littéral entier 0 se fera ensuite. Le résultat de la comparaison de deux entiers avec l'opérateur == sera booléen, et aura la valeur attendue (vrai si nb est pair, faux sinon).
À tirte de complément, notez que l'écriture correcte de est_impair() serait :
bool est_impair(int nb) {
return !est_pair(nb);
}
En exprimant est_impair() à partir de est_pair(), on limite les points de mise à jour de notre programme et on assure la cohérence mutuelle de ces deux opérations.
En 2018, mon illustre collègue Vincent Echelard a soumis l'extrait de code C# qui suit à mon attention :
// ...
if((Input.GetAxis("LT") == 0 ? false : true)
// ...
... ce qui, dans le même ordre d'idées, se simplifie pour donner :
// ...
if(Input.GetAxis("LT") != 0)
// ...
... qui est tellement plus simple et plus direct. Pourquoi se compliquer la vie?
La situation : on veut que la variable bOk reçoive la valeur true si les variables bDonneesRecues, bFinReception et bOk ont aussi la valeur true.
Avant d'aller plus loin, relevons qu'il y a une faute de logique de base dans l'énoncé ci-dessus, sur laquelle nous reviendrons un peu plus bas.
L'horreur : mêler les opérateurs == et =, ce qui est commun quand on est distrait (ou quand on navigue entre plusieurs langages, ce qui se produit quand on passe par exemple de VB à C++ ou de Pascal à C#) :
if ( bDonneesRecues = true &&
bFinReception = true &&
bOk = true) {
bOk = true;
}
Le bon usage (volet syntaxique): utiliser == pour les comparaisons logiques (plutôt que = qui est l'opérateur d'affectation de C, C++, C# et Java). Cela nous donne :
if ( bDonneesRecues == true &&
bFinReception == true &&
bOk == true) {
bOk = true;
}
Le bon usage (première simplification logique) : quand on veut vérifier si une variable booléenne est vraie, pas besoin de la comparer avec la valeur vraie (car b==true vaudra true si b est true et false si b est false... aussi bien examiner la valeur de b directement!).
if (bDonneesRecues && bFinReception && bOk) {
bOk = true;
}
Le bon usage (deuxième simplification logique) : on vérifie ci-dessus une expression logique, puis on donne la valeur true à bOk si et seulement la condition est vraie. Si la condition est fausse, alors bOk gardera sa valeur antérieure (true si bOk valait true, et false si bOk valait false).
On voit aussi que la condition ne peut être vraie que si au moins bOk est vrai (car si bOk est faux, alors la condition est nécessairement fause). On notera que si bOk était faux avant l'alternative, alors bOk restera faux après l'alternative aussi (le code devant être exécuté si la condition s'avère vrai n'étant jamais atteint).
Il est éclairant d'examiner une nouvelle écriture du même énoncé, faite cette fois à l'aide d'alternatives imbriquées.
if (bOk) {
// ici, bOk est déjà true!
if ( bDonneesRecues && bFinReception ) {
bOk = true;
}
} else { // ici, bOk est déjà false!
bOk = false;
}
Constat : si bOk valait vrai avant la condition, alors il vaudra encore vrai après la condition. De même, si bOk valait faux avant la condition, alors il vaudra encore faux après la condition. Ce bloc d'instructions est donc absolument inutile, sinon pour consommer – et gaspiller – des cycles d'exécution du processeur. Le bon usage serait de l'éliminer complètement du programme!
Remarque : quand on en arrive à un bloc d'instructions sans la moindre utilité (surtout si, à l'origine, ce bloc entraînait des erreurs à l'exécution!) il vaut la peine de s'arrêter et de se demander comment on en est arrivé(e) là.
Un truc : commenter systématiquement son code. Si on avait commenté de manière utile ce bloc (donc pas en indiquant « si bDonneesRecues vaut true et si bFinReception vaut true et si bOk vaut true, alors j'affecte la valeur true à bOk », ce qui ne nous dit rien sur ce que fait, vraiment, ce bloc de code, ce à quoi il sert dans le programme), alors on aurait eu de la difficulté à dire ce à quoi ce bloc de code sert. Cela nous aurait donné un fort indice quant au problème de fond ici: étant incapables de dire ce que fait le code, il aurait alors fallu se pencher sur sa raison d'être...
La situation : on souhaite trouver la première de nombreuses (22 dans ce cas-ci!) sous-chaînes dans une chaîne de caractères (note : ceci est un cas réel, et m'a été rapporté en 2020 par mon ami Billy Baker, membre comme moi de WG21 et de SG14).
Remarque : supposons que la signature de la fonction a été imposée, et va comme suit :
const bool find_next(const std::string &cur, std::string &next, size_t &next_idx, const std::string &message);
... où cur est XX, next est XX, next_idx est XX et message est XX. Vous remarquerez tout de suite des enjeux de conception :
La fonction retourne un const bool ce qui est strictement nuisible. En effet, retourner une copie d'un bool isole l'appelé de l'appelant, et qualifier ce bool de const ne fait que restreindre, sans quelque gain que ce soit, ce que l'appelant peut faire avec ce bool qui, concrètement, est devenu le sien
La fonction a trois extrants, soit la valeur de retour et deux paramètres passés par référence non-const (next et next_idx), ce qui complique la tâche de décrire clairement son rôle
BILLY BAKER (I don’t think I have a problem with you saying that I sent it to you with your caveat. Saying something like “Billy Baker, another committee member, SG14 member, …” rather than mentioning FlightSafety should be fine?), 2020
Full effect with anonymized names and strings. After looking at the whole function, this is the one and only horror that I was thinking was actually two. As you can see, more than one issue.
// find first of 22 different substrings within a substring of a user-provided string
const bool find_next(const std::string& cur, std::string& next, size_t& next_idx, const std::string& message) {
std::size_t a_f = message.substr(cur.size()).find(lit_1);
std::size_t b_f = message.substr(cur.size()).find(lit_2);
std::size_t c_f = message.substr(cur.size()).find(lit_3);
std::size_t d_f = message.substr(cur.size()).find(lit_4);
std::size_t e_f = message.substr(cur.size()).find(lit_5);
std::size_t f_f = message.substr(cur.size()).find(lit_6);
std::size_t g_f = message.substr(cur.size()).find(lit_7);
std::size_t h_f = message.substr(cur.size()).find(lit_8);
std::size_t i_f = message.substr(cur.size()).find(lit_9);
std::size_t j_f = message.substr(cur.size()).find(lit_10);
std::size_t k_f = message.substr(cur.size()).find(lit_11);
std::size_t l_f = message.substr(cur.size()).find(lit_12);
std::size_t m_f = message.substr(cur.size()).find(lit_13);
std::size_t n_f = message.substr(cur.size()).find(lit_14);
std::size_t o_f = message.substr(cur.size()).find(lit_15);
std::size_t p_f = message.substr(cur.size()).find(lit_16);
std::size_t q_f = message.substr(cur.size()).find(lit_17);
std::size_t r_f = message.substr(cur.size()).find(lit_18);
std::size_t s_f = message.substr(cur.size()).find(lit_19);
std::size_t t_f = message.substr(cur.size()).find(lit_20);
std::size_t u_f = message.substr(cur.size()).find(lit_21);
std::size_t v_f = message.substr(cur.size()).find(lit_22);
size_t lowest = std::min(a_f,
std::min(b_f,
std::min(c_f,
std::min(d_f,
std::min(e_f,
std::min(f_f,
std::min(g_f,
std::min(h_f,
std::min(i_f,
std::min(j_f,
std::min(k_f,
std::min(l_f,
std::min(m_f,
std::min(n_f,
std::min(o_f,
std::min(p_f,
std::min(q_f,
std::min(r_f,
std::min(s_f,
std::min(t_f,
std::min(u_f, v_f)
))))))))))))))))))));
if (lowest == std::string::npos) {
next_idx = std::string::npos;
next.clear();
return false;
} else if (lowest == a_f) {
next = lit_1;
} else if (lowest == b_f) {
next = lit_2;
} else if (lowest == c_f) {
next = lit_3;
} else if (lowest == d_f) {
next = lit_4;
} else if (lowest == e_f) {
next = lit_5;
} else if (lowest == f_f) {
next = lit_6;
} else if (lowest == g_f) {
next = lit_7;
} else if (lowest == h_f) {
next = lit_8;
} else if (lowest == i_f) {
next = lit_9;
} else if (lowest == j_f) {
next = lit_10;
} else if (lowest == k_f) {
next = lit_11;
} else if (lowest == l_f) {
next = lit_12;
} else if (lowest == m_f) {
next = lit_13;
} else if (lowest == n_f) {
next = lit_14;
} else if (lowest == o_f) {
next = lit_15;
} else if (lowest == p_f) {
next = lit_16;
} else if (lowest == q_f) {
next = lit_17;
} else if (lowest == r_f) {
next = lit_18;
} else if (lowest == s_f) {
next = lit_19;
} else if (lowest == t_f) {
next = lit_20;
} else if (lowest == u_f) {
next = lit_21;
} else if (lowest == v_f) {
next = lit_22;
} else {
next.clear();
next_idx = std::string::npos;
return false;
}
next_idx = lowest + cur.size();
return true;
}
Quelques liens pour enrichir le propos.