NR : Non‑Règles et Mythes

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 ».

Résumé des non‑règles :

NR.1 : Ne pas insister sur le fait que toutes les déclarations doivent se trouver en haut d’une fonction

Raisonnement

La 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.

Exemple – mauvais

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.

Alternative

NR.2 : Ne pas insister sur le fait d’avoir uniquement une seule instruction return dans une fonction

Raisonnement

La 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.

Exemple

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.

Exemple

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.

Alternative

NR.3 : Ne pas éviter les exceptions

Raisonnement

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 :

Exemple

???

Alternative

NR.4 : Ne pas insister sur le fait de placer chaque définition de classe dans son propre fichier source

Raisonnement

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.

Exemple

???

Alternative

NR.5 : Ne pas utiliser l’initialisation en deux phases

Raisonnement

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).

Remarque

On l’appelle parfois également construction en deux étapes.

Exemple – mauvais

// 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.  

Exemple – bon

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...  

Alternative

NR.6 : Ne pas placer toutes les actions de nettoyage à la fin d’une fonction et utiliser goto exit

Raisonnement

goto est source d’erreurs.
Cette technique constitue une approche « pré‑exceptions » pour la gestion des ressources et des erreurs similaire à RAII.

Exemple – mauvais

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.

Alternative

NR.7 : Ne pas rendre les membres de données protected

Raisonnement

Les 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.

Exemple

???

Alternative