Ne partez pas du principe que les accesseurs sont rapides

Article original : You shouldn’t assume accessors are fast | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

« Les accesseurs sont censés être rapides. » J’ai entendu ce leitmotiv tellement de fois au cours de ma carrière que je ne pouvais pas ne pas en parler.

Explication

À quoi servent les accesseurs ?

En faisant des recherches pour cet article, j’ai été surprise de constater qu’il y avait une définition formelle des accesseurs1.

Vous pouvez trouverez cette définition dans la section § 18.3.5 Accessor functions de la quatrième édition de The C++ Programming Language (Bjarne Stroustrup).

Pour faire court, la definition stipule que, même s’il est déconseillé d’en abuser, vous pouvez développer une méthode pour lire ou modifier les attributs privés d’une classe pour que votre algorithme (qui a besoin de lire/écrie ces données) reste simple et clair.

Donc, en clair, les accesseurs sont une interface entre les membres privés d’une classe et l’utilisateur de cette classe.

Comme toute interface, les accesseurs peuvent masquer la réelle manière donc la classe accède aux donnée voulues, encapsulant les comportement complexe pour que la classe soit simple à utiliser.

1. Je ne discute pas ici du fait qu’il existe une définition pseudo-consensuelle des accesseurs. Comme cette article vise à déconstruire cette définition, je ne construis pas mon argumentaire dessus. Elle est cependant (implicitement) décrite dans la section suivante, où j’explique pourquoi c’est une mauvaise définition.

Les gens n’utilisent pas les accesseurs correctement…

Tous les jours je vois des gens qui utilisent les accesseurs de manière impropre. Voici quelques exemples :

  • Certaines classes ont des attributs privés mais tous ces attributs ont des accesseurs simple get et set. Si votre class rend accessible toutes ses données sans distinction ni subtilité, alors ce n’est pas une class, c’est une struct2.
  • Certains accesseurs sont déclarés, implémentés, mais jamais utilisés. N’écrivez pas une fonction dont vous n’avez pas besoin, cela pollue la codebase.
  • Certaines personnes, quand elles constatent qu’elles ont besoin d’un accesseur qui n’existe pas, l’implémentent sans réfléchir. Parfois (souvent), si un accesseur n’est pas implémenté, c’est qu’il y a une bonne raison. Réfléchissez bien quand vous faites cela.
  • Certains getters ne sont pas const alors qu’ils le devraient. Par défaut, utilisez const partout, à moins que vous ayez spécifiquement besoin que ce soit non-const.
  • Certains getters sont artificiellement rendus const alors qu’ils ne devraient pas l’être. C’est un cas très rare, mais j’ai déjà vu plusieurs fois un développeur utiliser un const-cast juste pour rendre le getter const, bien que cela ne soit (dans ce cas de figure spécifique) pertinent. Vous devez éviter les const-cast à tout prix.

Il y a pas mal d’autres mauvaises pratiques qui existent mais qui sont trop verbeuses à décrire dans un article qui n’y est pas dédié.

Tout ça pour dire que beaucoup de gens n’utilisent pas les accesseurs correctement.

2. Ici, j’utilise abusivement et volontairement les termes class pour désigner une structure de donnée avec des membres privé et struct pour désigner un simple data bucket.

…ce qui mène à de mauvaises suppositions

Les pratiques incorrectes engendrent des états d’esprit incorrects, et Les états d’esprits incorrects engendrent les mauvaises suppositions.

La mauvaise supposition la plus répandue concernant les accesseurs est la suivante :

Tous les accesseurs doivent être rapides à exécuter

Partir du principe que les accesseurs sont rapide est une contrainte inutile

Sans parler du fait que ça peut être dangereux (si vous appelez une fonction en pensant qu’elle est rapide, le jour où ce n’est pas le cas vous aurez une mauvaise surprise), en faisant cette supposition vous vous infligez une contrainte.

Cette contrainte n’est pas réellement utile (tout ce que vous avez à y gagner, c’est l’économie d’aller vérifiez la documentation de l’accesseur, documentation que vous devriez dans tous les cas consulter). Cela restreint de ce fait votre capacité à innover et faire quelque chose d’utile de cet accesseur.

Illustration avec un pattern utile

Je vais vous montrer un pattern que j’aime beaucoup car il peut, dans certains cas, s’avérer très pratique.

Considérez cela : vous devez implémenter une classe qui, selon un certain nombre d’entrées, fournit plusieurs sorties (deux dans notre exemple). Vous devez utiliser cette classe pour remplir deux tableaux avec ces sorties. Cependant, les paramètres ne varient pas toujours, donc parfois, refaire le calcul est inutile. Comme le calcul à effectuer est coûteux (en temps d’exécution), vous ne voulez pas le relancer quand c’est inutile3.

Une façon (naïve) de le faire est ainsi (code complet à https://godbolt.org/z/a4x769jra) :

class FooBarComputer
{
public:
    FooBarComputer();
 
    // These update the m_has_changed attribute if relevant
    void set_alpha(int alpha);
    void set_beta(int beta);
    void set_gamma(int gamma);
 
    int get_foo() const;
    int get_bar() const;
 
    bool has_changed() const;
    void reset_changed();
 
    void compute();
 
private:
    int  m_alpha, m_beta, m_gamma;
    int m_foo, m_bar;
    bool m_has_changed;
};
 
 
//  ...
 
 
//==============================
bool FooBarComputer::has_changed() const
{
    return m_has_changed;
}
 
void FooBarComputer::reset_changed()
{
    m_has_changed = false;
}
 
 
//==============================
// Output getters
int FooBarComputer::get_foo() const
{
    return m_foo;
}
 
int FooBarComputer::get_bar() const
{
    return m_bar;
}
 
 
//  ...
 
 
//==============================
// main loop
int main()
{
    std::vector<int> foo_vect, bar_vect;
    FooBarComputer fbc;
 
    for (int i = 0 ; i < LOOP_SIZE ; ++i)
    {
        fbc.set_alpha( generate_alpha() );
        fbc.set_beta( generate_beta() );
        fbc.set_gamma( generate_gamma() );
 
        if ( fbc.has_changed() )
        {
            fbc.compute();
            fbc.reset_changed();
        }
 
        foo_vect.push_back( fbc.get_foo() );
        bar_vect.push_back( fbc.get_bar() );
    }
}

Cependant, il est possible d’écrire une meilleure version de cette classe en modifiant la manière dont les getters sont implémentés, comme ceci (code complet à https://godbolt.org/z/aqznsr6KP) :

class FooBarComputer
{
public:
    FooBarComputer();
 
    // These update the m_has_changed attribute if relevant
    void set_alpha(int alpha);
    void set_beta(int beta);
    void set_gamma(int gamma);
 
    int get_foo();
    int get_bar();
 
private:
    void check_change();
    void compute();
 
    int  m_alpha, m_beta, m_gamma;
    int m_foo, m_bar;
    bool m_has_changed;
};
 
 
//  ...
 
 
//==============================
void FooBarComputer::check_change()
{
    if (m_has_changed)
    {
        compute();
        m_has_changed = false;
    }
}
 
 
//==============================
// Output getters
int FooBarComputer::get_foo()
{
    check_change();
    return m_foo;
}
 
int FooBarComputer::get_bar()
{
    check_change();
    return m_bar;
}
 
 
//  ...
 
 
//==============================
// main loop
int main()
{
    std::vector<int> foo_vect, bar_vect;
    FooBarComputer fbc;
 
    for (int i = 0 ; i < LOOP_SIZE ; ++i)
    {
        fbc.set_alpha( generate_alpha() );
        fbc.set_beta( generate_beta() );
        fbc.set_gamma( generate_gamma() );
 
        foo_vect.push_back( fbc.get_foo() );
        bar_vect.push_back( fbc.get_bar() );
    }
}

Voici les avantages de la seconde version :

  • Le code du main est plus concis est clair.
  • Le compute() n’a plus besoin d’être public.
  • Vous n’avez plus besoin du has_changed(), à la place vous avez check_change() mais qui est privé.
  • L’utilisateur de votre classe sera moins enclin à mal l’utiliser. Il ne pourra pas appeler le compute() à tout bout de champs, puisque celui-ci est devenu paresseux.

C’est ce dernier point qui est le plus important. N’importe quel utilisateur, dans la première version, aurait pu omettre la conditionnelle et appeler le compute() à chaque tour de boucle la rendant inefficace.

3. Si vous le souhaitez, vous pouvez imaginer que les paramètres changent environ une fois sur cent. Ainsi, il est important de ne pas refaire le calcul quand celui-ci est inutile.

N’est-il pas possible de faire autrement ?

L’argument contre cette pratique que j’ai le plus entendu est le suivant : « Et bien, pourquoi tu ne renomme pas tout simplement le getter ? Comme computeFoo() par exemple ? ».

Et bien, le getter ne fait pas toujours un compute(), c’est donc impropre de l’appeler ainsi. De plus, sémantiquement, le mot compute signifie « faire une opération », avec comme sous-entendu qu’elle ne retourne pas de valeur (au mieux un code d’erreur). Et même si certains développeurs le font, je n’aime pas utiliser ce mot ainsi.

« Dans ce cas, appelle-la computeAndGetFoo() ! »

Sauf que, une fois encore, elle ne fait pas toujours un conpute(). Si on voulait être exhaustif (ce qu’on doit toujours essayer d’être d’après moi), on devrait l’appeler sometimesComputeAndAlwaysGetFoo(), ce qui juste ridicule pour une méthode aussi simple.

« Alors vas-y, trouve un nom adéquat ! »

C’est chose faite. Ce nom est getFoo(). C’est exactement ce que ça fait : ça get le foo. Le fait que le compute() est paresseux ne change rien au fait que c’est un getter. De plus, il est mentionné en commentaire que le get peut être coûteux à exécuter, donc lit la documentation et tout se passera bien.

« Et on ne pourrait pas mettre la vérification dans compute() au lieu de dans le getter ? »

On pourrait, mais quel serait l’intérêt ? Rendre le compute() public est inutile puisqu’on a forcément besoin du getter pour accéder aux données, et on ne veut exécuter le compute() que si on s’en sert.

Il est possible de faire autrement…

À vrai dire, on peut effectivement faire autrement, ce qui est très pratique si, par exemple, on veut ajouter un getter pour lequel veut être sûr qu’il n’appellera jamais le compute() (nécessaire si vous avez besoin d’un getter constant).

Dans ce cas, je conseille d’utiliser deux noms différents pour les deux getters, car ils ne font pas tout à fait la même chose. Leur donner le même nom (avec seulement le mot-clé const comme différence) sera déroutant.

Personnellement, j’utilise cette graphie :

  • getActiveFoo()
  • getPassiveFoo()

J’aime bien cette façon d’écrire car on indique explicitement que le getter est potentiellement coûteux ou pas. De plus, cela indique implicitement que le getter passif peut vous donner une valeur périmée de foo.

En outre, quiconque tente d’appeler getFoo() se confrontera à une erreur de compilation et devra choisir entre une des deux version, le forçant à réfléchir. C’est toujours une bonne chose de forcer l’utilisateur à réfléchir.

Le plus important : propagez la bonne parole

Puisqu’il est possible de mettre du code lent dans des getters, vous trouverez forcement des développeurs qui le feront.

Le comportement le plus dangereux à adopter et d’ignorer ce fait et laisser le gens penser que les accesseurs sont rapide en exécution.

Vous devriez prévenir vos collègues que les accesseurs sont, au final, comme n’importe quelle autre méthode : ils peuvent être lent et tout le monde doit lire leur documentation avant de les utiliser.

Dans cet article, je parle surtout de temps d’exécution, mais mon propos s’applique aussi à toutes les autres formes de performances : taille mémoire, accès disque, accès réseau, etc.

Merci de votre attention et à la semaine prochaine !

Article original : You shouldn’t assume accessors are fast | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Combien de langages un expert (en C++) doit savoir parler ?

Article original : How many languages should a (C++) expert speak? | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Cet article est une relecture du Lightning Talk que j’ai donné à l’occasion de la CppCon 2020 : How many languages a (C++) expert should speak ? – Thomas Lourseyre – CppCon 2020 – YouTube

Je vais essayer d’élaborer ce que cet interlocuteur (au demeurant stressé et fébrile) essaie de montrer dans cette présentation.

Pourquoi apprendre d’autres langages ?

Je vais partir du principe que vous êtes un(e) développeur(se) C++ et expliquer pourquoi vous devez connaître d’autres langages de programmation en utilisant deux citations.

Si tout ce que vous avez est un marteau, tous les problèmes ressemblent à un clou.
– Marteau de Maslow

Disons qu’on vous donne la tâche de résoudre un problème, un fait technique ou un dilemme de programmation. Une des premières questions que vous aurez à vous poser sera « Quel langage devrais-je utilise pour résoudre cela ? ». Si vous ne connaissez que le C++, vous ne pourrez répondre que « C++ ». Mais, de manière assez évidente, le C++ ne peut pas être toujours la bonne réponse.

Même si vous n’êtes pas expert(e) dans d’autres langages, si vous connaissez les bases et êtes capable de dire « Conceptuellement, ce langage est plus adapté à ce problème », alors vous n’aurez plus qu’à trouver un expert dans ledit langage et le tour sera joué. C’est une forme de recul que vous ne pouvez pas avoir si vous ne vous intéressez qu’à un seul langage.

Ceux qui ne peuvent se souvenir du passé sont condamnés à le revivre.
– George Santayana

L’industrie du développement logiciel n’est pas un paradis rose et sucré où tous les projets commencent avec tous les outils dont on peut rêver dans leur version la plus récente. Parfois (voire souvent, en fonction de comment vous vous positionnez dans l’industrie) vous devrez travailler sur du code legacy. Parfois, il sera écrit en C++ et vous devrez le porter dans un autre langage. Et parfois, il sera écrit dans un autre langage et vous devrez le traduire en C++.

Dans tous les cas, vous ne pourrez faire ce travail que si vous connaissez d’autres langages que le C++.

Posons la vraie question

Tout cela étant dit, je peux affirmer sans crainte que la vraie question n’est pas « Devrais-je apprendre d’autres langages ? » mais plutôt :

Combien d’autres langages devrais-je apprendre ?

Le tableau de répartition des connaissances

J’ai conçu un petit tableau permettant de classer les langages de programmation qu’une personne connaît en fonction de leur niveau de connaissance :

Niveau de connaissanceDétailsExemples personnels
ExpertC’est pas grave si vous n’en connaissez qu’un seulC++
PratiqueLes langages que vous aimez et pratiquez sur une base régulière.Python, C, Rust, Bash
DocumentationLes langages que vous avez pratiqué mais que vous n’aimez pas. Vous êtes capable vous documenter.Java, C#, js, basic, perl
Hello word!Vous ne connaissez pas grand chose à leur propos.Ruby, AS, Go, etc.

Vous pouvez d’ailleurs remarquer (en comparant avec la vidéo) que mes exemples personnels ont évolué depuis l’année dernière – ce qui est rassurant.

Les niveaux de connaissance

  • Expert : Ce sont les langages pour lesquels vous avez une expertise poussée et fiable. Beaucoup de développeurs ne sont pas particulièrement experts et la plupart de ceux qui le sont n’ont qu’un seul langage d’expertise. Il est possible d’en avoir plusieurs bien sûr, mais c’est assez rare et requiert beaucoup d’efforts.
  • Pratique : Ce sont les langages que vous pratiquez sur une base régulière, sans pour autant en être un(e) expert(e). Souvent ce sont des langages que vous aimez bien et que vous utilisez lors de vos projets personnels.
  • Documentation : Il y a probablement des langages que vous avez essayés, mais qui ne vous ont pas spécialement plu. Ou bien des langages que vous avez dû apprendre pour un projet spécifique, mais vous êtes empressé(e) d’oublier après. Ce sont des langages dont vous connaissez au moins les bases et pour lesquels vous êtes capable de vous documenter si nécessaire.
  • Hello world! : Ce sont les langages dont vous ne savez que très peu de choses. Souvent vous les connaissez de nom, peut-être avez déjà rédigé un Hello world!, mais sans plus. Vous n’êtes ainsi pas capable de citer les spécificités fondamentales du langage.

Les seuls niveaux de connaissances utiles sont expertpratique et documentation. À partir du moment où vous êtes capable de savoir où et comment consulter la documentation d’un langage, vous pouvez en faire quelque chose. Le niveau Hello world! est lui inutile.

Comment promouvoir un langage

Votre but est donc maintenant de « promouvoir » les langages pour les faire monter dans le tableau. Mais les efforts nécessaires à cela varient en fonction de là où se trouve chaque langage :

  • De pratique à expert : Devenir un expert est une tâche longue et difficile, quel que soit le domaine. Vous n’avez pas à devenir expert(e) dans un langage à moins que vous ne vouliez y dédier une partie conséquente de votre carrière ou de votre temps libre. C’est un gros gaspillage d’effort de devenir expert dans un langage juste pour être plus polyvalent.
  • De documentation à pratique : Si vous vous forcez à pratiquer un langage que vous n’aimez pas, il y a une bonne chance pour que vous finissiez par le détester encore plus. Bien sûr, vous pouvez toujours avoir une bonne surprise (et vous rendre compte que vous aimez un langage que vous détestiez au départ), mais cela n’arrivera pas souvent. On ne peut pas forcer l’amour, si vous tentez de sur-pratiquer un langage que vous n’aimez pas pour être plus polyvalent est une forme de souffrance assez inutile.
  • De Hello world! à documentation : Qu’est-ce qu’il en coûte d’essayer un langage dont vous ne connaissez que peu de choses dans le but de vous faire un avis ? En l’espace de quelques heures, vous pourrez lire la note d’intention du langage, apprendre les bases et savoir où se trouve la documentation. Peut-être n’aimerez-vous pas ce langage, mais au moins vous en saurez un peu plus à son propos.

La promotion la plus rentable est la dernière, de Hello world! à documentation. Si vous êtes curieux(se) et essayez d’apprendre le pourquoi du comment du plus grand nombre de langages possibles, alors vous aurez une vue beaucoup plus large du monde de la programmation.

Du coup, combien de langages devriez vous savoir parler ?

La réponse que je vais vous donner aujourd’hui est la suivante : autant que ce que votre curiosité vous le permet.

Une des plus grandes qualités qu’un expert peut avoir (que ce soit en C++ ou non) est sa curiosité. Ce qui nous pousse à en apprendre plus, chaque jour, à propos de n’importe quel sujet. Tant que vous serez curieux(se), vous vous enrichirez et grandirez.

Ne gaspillez pas votre curiosité sur un unique sujet, la curiosité est une qualité mieux employée en largeur qu’en profondeur. D’après moi, un bon expert est aussi un polytechnicien.

Avec quelle régularité devriez-vous apprendre des nouveaux langages et entretenir ceux que vous connaissez ?

La réponse à cette question est très dépendante du temps libre que vous voulez allouer à la programmation.

Si, à un moment donné, vous avez juste une heure de temps libre que vous voulez investir dans la programmation, prenez un langage dont vous ne connaissez rien (ou presque). Lisez la note d’intention et écrivez un petit programme simple. Normalement, ce sera suffisant pour que vous sachiez si vous voulez y investir plus de temps ou non. Si c’est non, gardez en tête comment accéder à la documentation, ça pourra toujours être utile.

Si un langage que vous venez de découvrir vous plaît, essayez d’évaluer combien de temps vous pouvez passer pour vous y entraîner. Vous pouvez faire des tutoriels, des exercices, des projets personnels.

Il y a un bon moyen de s’entraîner dans vos langages favoris : les plateformes comme CodinGame vous propose de l’entraînement, des défis et des compétitions de codage. Essayez, c’est très addictif !

Coding Games and Programming Challenges to Code Better (codingame.com).

Est-ce un exercice réservé aux experts ?

Je peux entendre supprimer certains d’entre vous derrière votre écran, « Je ne suis même pas expert(e) en C++, dois-je vraiment prendre la peine d’apprendre un autre langage ? ».

Tous les arguments que j’ai détaillés dans cet article sont valables pour les non-experts aussi bien que pour les experts. La différence est que, selon mon expérience personnelle, les personnes « non-expertes » sont plus à même de changer de langage au cours de leur carrière. Connaître plusieurs langages vous permettra donc d’avoir de l’avance et un peu plus de recul sur les possibilités qui pourront s’offrir à vous.

La curiosité est une excellente qualité, dans le développement logiciel.

Merci de votre attention et à la semaine prochaine !

Article original : How many languages should a (C++) expert speak? | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Est-ce que toutes les variables devraient être const par défaut ?

Article original : Should every variable be const by default? | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Un peu de terminologie

Dans le titre de l’article, j’utilise le mot const car const est un mot-clé que tous les développeurs C++ connaissent (enfin j’espère). Cela dit, le concept qui est adressé ici porte un nom plus générique, il s’agit de l’immuabilitéconst est un mot-clé utilise dans certains langages (comme le C++ ou le Javascript par exemple) mais la notion d’immuabilité existe aussi dans d’autres langages (qui parfois n’ont même pas de mot-clé équivalent à const, comme en Rust par exemple).

Ainsi, afin d’être inclusif, j’utiliserai les mots immuable et immuabilité à la place des mots constant et constance.

Nota bene : en C++ le mot-clé const ne représente pas exactement la notion d’immuabilité. Par exemple, vous pouvez utiliser le mot-clé const dans le prototype d’une fonction pour indiquer que vous n’allez pas le modifier, mais vous pouvez tout à fait passer une variable muable à cette fonction en guise de paramètre. Cela étant dit, cet article s’en réfère à const pour son caractère à rendre les données immuables, donc je vous saurais gré d’ignorer les cas où const ne s’en réfère pas à des données qui sont immuables de manière inhérente. C’est aussi une autre raison pour laquelle je préfère dire immuable et non pas constant.

Quelques très bonnes raisons de rendre vos variables immuables

Sécurité

Ce qu’on appelle la const-correctness (qui peut être maladroitement traduit en correction de constance, avec « correction » au sens de « qui est correct ») est une notion importante en C++ qui correspond au fait que si vous spécifiez qu’une variable est immuable, alors elle ne sera pas modifiée.

La const-correctness est atteinte lorsque vous utilisez le mot-clé const pour indiquer quelles variables ne doivent pas être modifiées. Ainsi, vous ne pourrez pas modifier par inadvertance des données qui n’ont pas à l’être et cela donne une indication sémantique aux autres développeurs et au compilateur.

Documentation

Ces cinq petits caractères en disent long. Si vous vous trouvez dans un code digne de confiance, la présence -ou l’absence- du mot-clé const donne des informations importantes sur les intentions derrière telle ou telle variable.

Optimisation

Le compilateur peut (et va) optimiser le code en fonction des directives d’immuabilité

En sachant si telle ou telle variable est immuable, le compilateur pourra faire des suppositions et prendre des raccourcis.

Si vous voulez en savoir plus à ce sujet je vous conseille le talk de J. Turner : Jason Turner: Practical Performance Practices – YouTube

Sources

Si vous n’êtes toujours pas convaincu(e) que l’immuabilité est utile (au bas mot), ou si vous êtes juste curieux(se), vous pouvez lire les articles suivants :

Y a-t-il des inconvénients à utiliser l’immuabilité dès que possible ?

Comme l’immuabilité est une restriction technique, il n’y a aucun inconvénient technique à l’utiliser.

Cependant, il peut y avoir des inconvénients plus subjectifs. Premièrement, cela augmente un peu la verbosité du code. Même s’il ne s’agit que de cinq caractères (qui dans la plupart des IDE prennent la même couleur que le type associé), certaines personnes peuvent trouver cela gênant. Il y a également une forme d’inertie qui peut apparaître : si à un moment donné vous avec besoin que la donnée soit muable, alors vous devrez aller chercher sa déclaration et la modifier. Certains considèrent cela comme une mesure de sécurité, d’autres comme un inconvénient.

Est-ce que ces arguments sont suffisants pour ne pas utiliser l’immutabilité dès qu’on le peut ? C’est à vous de juger, mais en mon humble avis, je ne pense pas.

Et les autres langages ?

D’autres langages ont effectivement rendu les variables immuables par défaut.

C’est le cas du Rust. En Rust, vous n’avez pas d’équivalent du mot-clé const, toutes les variables sont immuables par défaut. Au contraire, vous avez le mot-clé mut (pour mutable, la traduction anglaise de muable) que vous devez apposer devant chaque variable muable.

Par exemple, le code suivant ne compile pas en Rust :

fn main() {
    let foo = 2;
    println!("Foo: {}", foo);
    foo += 40; // error[E0384]: cannot assign twice to immutable variable `foo`
    println!("Foo: {}", foo);
}

Mais il faut plutôt écrire ceci :

fn main() {
    let mut foo = 2;
    println!("Foo: {}", foo);
    foo += 40;
    println!("Foo: {}", foo);
}

La raison mentionnée dans le manuel Rust est la suivante (je traduis) : « C’est un des nombreux coups de pouce que le Rust vous donne pour écrire du code qui tire profit de la sécurité […] que le langage a à offrir. »

Rust est un langage moderne qui vise à produire du code sécurisé tout en étant performant. Dans cette optique je suis d’accord avec le choix qu’il a fait de rendre les variables immuables par défaut pour la sécurité que cela apporte.

Est-il raisonnable de changer le standard pour rendre toutes les variables immuables par défaut ?

Si un jour fatidique, le C++ committee décide de rendre toutes les variables immuables par défaut… personne ne migrera vers cette nouvelle version du standard.

La rétro-compatibilité est importante pour adoucir les migrations. Il y a des exemples historiques de suppression de fonctionnalité du standard, mais à chaque fois il y avait une très bonne raison pour cela (la plupart du temps étant que cette fonctionnalité était obsolète depuis des lustres).

On ne peut pas sérieusement penser à enlever const du standard et le remplacer par un mut juste pour égard à ce qui est, en définitive, du sucre syntaxique.

Que faire alors ?

Pour ma part, j’utilise le mot-clé const systématiquement sans réfléchir, chaque fois que je déclare une variable. Je ne l’enlève que lorsque j’écris une instruction qui requiert effectivement que la variable soit muable (ou quand le compilateur me crie que j’essaye de modifier une variable constante). C’est un peu brut, mais selon moi c’est le meilleur moyen d’acquérir le bon réflexe de mettre const dès que l’on peut.

Bien sûr, vous pensez sans doute qu’on peut être plus malin et en essayant de prévoir si la variable sera mutable dès sa déclaration, mais je peux garantir que vous ne serez pas 100% fiables et vous laisserez passer des opportunités de rendre des données immuables (du moins, tant que vous n’aurez pas pris cette bonne habitude).

Voici donc mon conseil : jusqu’à ce que vous ayez le réflexe de mettre des const partout où vous les pouvez, utilisez const systématiquement et pour toutes les variables. Au pire vous aurez quelques erreurs de compilation facile à corriger, au mieux vous attraperez une bonne habitude.

Et quid de constexpr ?

constexpr est un mot-clé qui permet d’indiquer qu’une expression peut être évaluée au moment de la compilation. Le but est d’essayer de gagner du temps d’exécution en faisant un plus gros travail à la compilation.

En un sens, c’est une immutabilité plus forte que const. Tout ce que j’ai dit dans cet article peut s’appliquer à constexpr : utilisez-le dès que c’est possible.

Cependant, contrairement à const, je ne vous conseille pas d’être spécialement trop zélé(e)s avec constexpr.
Il est plus rare de pouvoir l’utiliser et les cas d’utilisation sont un peu plus évidents que son homologue. Mais gardez bien en tête d’utiliser constexpr dès que possible.

Merci de votre attention et à la semaine prochaine !

Article original : Should every variable be const by default? | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

N’utilisez pas les boucles brutes

Article original : Don’t use raw loops | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Note d’intention

Sean Parent a un jour dit « No raw loops », pas de boucle brute. C’était il y a huit ans.

Aujourd’hui, si vous n’avez pas un bagage C++ extrêmement fort, vous n’avez sans doute jamais entendu cette maxime (ni même entendu parler de Sean Parent). cela fait qu’aujourd’hui, en 2021, beaucoup de projets C++ utilisent des tonnes de boucles brutes et pratiquement aucun algorithme.

Cet article est destiné à vous, les développeuses et développeurs qui utilisez des boucles brutes en 2021, et va expliquer pourquoi et comment utiliser des algorithmes à la place.

Ressources

Ce sujet a été couvert par nombre d’experts ces dernières années. Si vous vous sentez d’attaque pour du contenu très technique et en anglais, suivez les liens suivants :

L’exposé de Sean Parent où il parle (en autres) des boucles brutes : C++ Seasoning | GoingNative 2013 | Channel 9 (msdn.com)

Un exposé de Jason Turner qui parle de « code smells » (ce qui inclue les boucles brutes) :
C++ Code Smells – Jason Turner – CppCon 2019 – YouTube

L’exposé de Jonathan Boccara à propos des algorithmes de la STL : CppCon 2018: Jonathan Boccara “105 STL Algorithms in Less Than an Hour” – YouTube

En bonus, une carte (littérale) où sont représentés les algorithmes de la STL :
The World Map of C++ STL Algorithms – Fluent C++ (fluentcpp.com)

Les boucles brutes : c’est quoi ?

Ce que j’appelle boucles brutes ou dans leur version originale raw loops sont les boucles telles que vous les imaginez : les mots-clés forwhile ou encore do while rattachés à des blocs de code.

Les boucles brutes sont opposées aux algorithmes, qui sont des boucles (ou pas ?), encapsulées dans des fonctions. Vous appelez ensuite ces fonctions avec tels ou tels arguments en fonction de l’usage que vous voulez faire de cet algorithme.

Pourquoi vous ne devriez pas utiliser les boucles brutes

C’est une question de sémantique. Une boucle brute n’exprime pas un but, mais une manière technique de procéder. Elles expriment comment votre algorithme fonctionne.

Quand vous appelez un algorithme, vous décrivez une intention, vous écrivez ce que vous voulez obtenir.

Par exemple, jetons un œil à ce morceau de code :

//...
 
for (size_t i = 0; i < my_vect.size() && predicate(my_vect, i) ; ++i)
{
    //...
}
 
//...

Cela vous indique que le programme effectue une boucle, indexée sur un vector et contrôlée par un prédicat personnalisé. Mais ça ne dit pas ce que la boucle réalise et quel est le résultat attendu : vous devez plonger dans le corps de la boucle pour le savoir.

Regardons maintenant ce morceau de code :

//...
 
auto it_found = std::find_if(cbegin(my_vect), cend(my_vect), predicate);
 
//...

Même si vous ne savez pas comment find_if() fonctionne en interne, vous comprenez aisément qu’elle revoie un élément de my_vect qui vérifie la condition predicate(). Vous ne savez pas comment ça marche, mais vous savez ce qui est renvoyé.

À partir de là, il faut prendre en compte plusieurs points :

  • Les algorithmes élèvent le niveau d’abstraction et vous aide à comprendre les intentions cachées derrière les lignes de code.
  • Une sémantique adéquate améliore la lisibilité, une meilleure lisibilité rend le code plus maintenable, un code mieux maintenable est moins sujet à des régressions.
  • Appeler un algorithme est souvent moins verbeux que le réécrire.
  • Les boucles pures sont sujette à des erreurs assez communes, comme les dépassements-de-un, les boucles vides, la complexité naïve, etc.

Que faire quand il n’existe pas d’algorithme adapté ?

Il arrive parfois qu’aucun algorithme existant ne corresponde parfaitement à votre besoin. Dans ce cas-là, que faire ?

Combiner des algorithmes pour en faire un nouveau

Souvent, votre algorithme spécifique est simplement une combinaison de deux algorithmes existants ou une spécification d’un algorithme déjà existant. Dans ce cas, il suffit de l’implémenter dans une fonction dédiée, en prenant bien soin de lui donner un nom clair et explicite.

Par exemple : vous devez vérifier que, dans un vector, tous les éléments qui remplissent la condition A remplissent aussi la condition B. Pour faire cela, vous pouvez utiliser l’algorithme std::all_of() avec un prédicat personnalisé :

template< typename Iterator, typename PredA, typename PredB >
bool both_or_none( Iterator first, Iterator last, PredA & predA, PredB & predB )
{
    auto pred = [&predA,&predB](const auto& elt)
    {
        return predA(elt) == predB(elt); 
    };
    return all_of(first, last, pred);
}

Le corps de cet algorithme est assez court : il crée une fonction combine nos deux prédicats pour implémenter notre condition spécifique, puis applique l’algorithme std::all_of(), qui vérifie que cette condition est vraie pour tous les éléments de la collection.

Enrober une boucle pure dans une fonction

Si vraiment vous n’avez aucun moyen de combiner les algorithmes qui ne soit pas trop forcé ou qui ne fasse pas artificiel, vous n’avez plus qu’à implémenter votre boucle brute en l’encapsulant dans une fonction dédiée.

Ainsi, vous aurez implémenté votre propre algorithme, qui pourra être appelé par quiconque en a besoin. N’oubliez pas de lui donner un nom clair et explicite.

Par exemple: vous avez une collection et avez besoin de savoir, parmi tous les éléments qui respectent une condition donnée, lequel est le plus grand d’entre eux. En somme, cela correspondrait à l’algorithme max_if() s’il existait.

Ce comportement est compliqué à implémenter en n’utilisant que la STL, car il faudrait réussir à détacher le sous-ensemble de la collection qui valide la condition pour pouvoir lui appliquer l’algorithme std::max() derrière. Hors, le seul algorithme permettant de faire cela est std::copy_if(), qui copie les éléments. Or, les copies peuvent être coûteuses, donc on ne veut pas faire ça.

Que faire alors ? Écrivons une boucle qui implémente ce max_if() nous-même, en l’encapsulant correctement :

template< typename Iterator, typename Pred >
constexpr Iterator max_if( Iterator first, Iterator last, Pred & pred )
{
    Iterator max_element = last;
    for (auto it = first ; it != last ; ++it)
    {
        if (pred(*it) && (max_element == last || *it > *max_element))
            max_element = it;
    }
    return max_element;
}

Dans le reste du programme, l’utilisation de max_if() sera sémantiquement explicite, avec tous les avantages qu’apportent les algorithmes.

Quelques exemples d’algorithmes de la STL

Il y a beaucoup d’algorithmes dans la STL. Je vous suggère d’être curieux(se) et de l’explorer par vous-même : Algorithms library – cppreference.com

En tant que mise en bouche, voici une petite liste d’algorithme que, si vous ne les connaissez pas déjà, vous devriez apprendre à connaître :

  • std::find() : Recherche un élément égal à une valeur donnée.
  • std::find_if() : Recherche un élément qui vérifie une condition donnée.
  • std::for_each() : Applique une fonction donnée à tous les éléments de la collection.
  • std::transform() : Applique une fonction donnée à tous les éléments de la collection et stocke le résultat dans une autre collection.
  • std::all_of() : Vérifie si tous les éléments de la collection vérifient un prédicat donné.
  • std::any_of() : Vérifie si au moins un élément de la collection vérifient un prédicat donné.
  • std::copy_if() : Copie les éléments de la collection s’ils vérifient une condition donnée.
  • std::remove_if() : Enlève le premier élément de la liste qui vérifie une condition donnée.
  • std::reverse() : Inverse l’ordre des éléments dans la collection.
  • Et bien plus…

Si vous voulez aller plus loin, il y a une présentation d’une heure qui présente plus de cent algorithmes de la STL : CppCon 2018: Jonathan Boccara “105 STL Algorithms in Less Than an Hour” – YouTube

En conclusion

Beaucoup d’experts en C++ sont d’accord pour dire que les boucles sont vouées à disparaître dans les plus hautes couches d’abstraction, et ne seront utilisées que pour les algorithme de plus bas niveau. Cette déclaration n’est pas absolue, mais un but à poursuivre, un idéal à garder en tête quand on code.

Si comme beaucoup de développeur(se)s C++ vous avez tendance à utiliser des boucles brutes au lieu d’algorithmes, vous devriez aller faire un tour dans les ressources que j’ai mentionnées au début de l’article. Comme vous vous familiariserez avec eux et les utiliserez de plus en pratique, vous les trouverez de plus en plus commodes.

Merci pour votre attention et à la semaine prochaine !

Article original : Don’t use raw loops | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Les exceptions sont des goto déguisés

Article original : Exceptions are just fancy gotos | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

À propos de goto

Retour aux bases : pourquoi est-ce que les goto sont mauvais ?

Pour répondre à cette question, il me suffirait de vous jeter un lien vers l’article de E. Dikjstra, Go To Statement Considered Harmful (arizona.edu), qui explique plutôt bien pourquoi les goto sont l’incarnation du mal, mais j’ai envie d’aller un peu plus loin.

Je vais donner ici une liste de raison pour lesquelles il ne faut pas utiliser goto, résumant ainsi l’article susmentionné tout en y apportant une touche de modernisation :

  1. Les goto sont des structures de contrôle non structurées, contrairement aux autres structures de contrôle en C ++. Les if et les boucles sont géographiquement liées au bloc de code qu’ils contrôlent (avec leur propre cycle de vie). Quand on les lit, on comprend aisément où le bloc de contrôle commence et où il finit. Les fonctions, qui sont une autre structure de contrôle, sont des points d’accès. Elles suivent un contrat dont le compilateur assure la validité. On leur donne une entrée, elles nous renvoient une sortie. Les goto, eux, ne sont pas structurés : ils correspondent à un aller simple dans le code, sans être géographiquement proche et sans assurer un système d’entrée-sortie comme les fonctions. Cela est sujet à produire du code spaghetti peu maintenable.
  2. Considérant ce que cela implique, le goto est probablement le mot-clé le moins sécurisé du langage. Je ne donnerais pas d’exemple expansif dans cet article, mais le nombre de choses autorisées avec les goto est surprenamment élevé. Si vous n’êtes pas assez vigilant, vous pourrez provoquer des crashs, parfois aléatoires, du jardinage mémoire, des comportements indéfinis, etc.
  3. Ici et maintenant, à l’ère du C ++ moderne, plus personne n’utilise les goto. Ça signifie que presque plus personne n’en est familier. La plupart des développeurs sait qu’il ne faut pas l’utiliser, mais assez peu d’entre eux savent pourquoi. De ce fait, si jamais d’aventure ils en croisent un dans une codebase, ils vont soit tenter de refactorer le code (avec le risque qu’ils comprennent la fonctionnalité de travers et causent une régression), soit le laisser en plan sans essayer de le comprendre. Étant donné qu’il est compliqué à utiliser, le fait que personne n’utilise le goto sur une base régulière est un argument de plus contre son utilisation.

Il existe des contextes spécifiques dans certains langages où le goto peut être considéré comme une bonne pratique, mais pas en C ++ moderne.

À propos des structures de contrôle et du code spaghetti

Dans la section précédente, le premier point (qui constitue l’argument principal contre le goto) parle de « structure de contrôle » et de « code spaghetti ».

De manière imagée, on pourrait voir l’exécution d’un programme comme une corde que l’on déroule le long du code, à mesure qu’il est exécuté. Tant que ce sont des instructions classiques qui sont exécutées, la corde se déroule normalement. Mais lorsqu’on atteint une structure de contrôle, cela se passe un peu différemment.

Quand l’exécution atteint une boucle for ou while, alors la corde effectue elle-même des boucles autour du bloc de code concerné. Quand l’exécution atteint une conditionnelle (if ou else), alors la corde passe au-dessus du code si nécessaire (en fonction de la condition vérifiée) pour continuer juste après.

Quand l’exécution atteint un appel de fonction, alors un brin de la corde se détache, forme un lacet qui va suivre le corps de la fonction puis revenir à la corde, là où elle l’a quitté.

Par contre, quand l’exécution atteint un goto, alors parfois la corde n’aura d’autre choix que d’être tendue à travers tout le code, pour atteindre le label associé. S’il y a plusieurs goto dans le code, alors la corde sera tendue à plusieurs endroits et dans tous les sens.

Si vous avez de bonnes structures de contrôle, alors vous pourrez suivre aisément votre corde du début à la fin, sans jamais être perdu. Mais si vos structures de contrôle sont chaotiques, alors la corde va se croiser partout et partir dans tous les sens. Elle ressemblera alors à un plat de spaghetti. C’est ça qu’on appelle avoir un code spaghetti.

Comme le goto a tendance à tendre la proverbiale corde à travers tout le code, son utilisation est facilement sujette à du code spaghetti.

Mais est-ce que le goto est vraiment malveillant ?

En prenant en compte tout ce que l’on vient de dire, ne pourrait-on pas quand même imaginer une manière sécurisée d’utiliser le goto ? Après tout, goto n’est malveillant que s’il provoque du code spaghetti. Mais si on réalise un code qui n’utilise le goto que localement, dans un espace contrôlé, alors le code ne sera pas spécialement spaghetti-esque, n’est-ce pas ?

Il existe effectivement des designs qui utilisent le goto pour rendre le code plus clair qu’il ne le serait avec d’autres structures de contrôle.

L’exemple le plus classique est celui des boucles imbriquées :Capture1.P

//...
 
bool should_break = false;
for (int i = 0 ; i < size_i ; ++i)
{
    for (int j = 0 ; j < size_j ; ++j)
    {
        if (condition_of_exit(i,j))
        {
            should_break = true;
            break;
        }
    }
    if (should_break)
        break;
}
 
//...

Si on écrit le même code avec un goto, ça sera un peu plus court et plus clair :Capture2.PNG888x314

// ...
 
for (int i = 0 ; i < size_i ; ++i)
{
    for (int j = 0 ; j < size_j ; ++j)
    {
        if (condition_of_exit(i,j))
            goto end_of_nested_loop;
    }
}
end_of_nested_loop:
 
// ...

Vous voyez ? goto n’est pas si inutile que ça !

Du coup, devrait-on utiliser ce mot-clé dans ces cas très spécifiques où il rend le code plus clair ? Je ne pense pas.

Il est au final assez compliqué de trouver des exemples variés où le goto est meilleur que les autres structures de contrôle, et les boucles imbriquées similaires à celle présentée ici sont rares. Et même si, dans cet exemple, le code est plus court, personnellement je ne le trouve pas plus clair pour autant. Il y a 2 niveaux de bloc de différence entre le goto et son label, ce qui à la lecture est contre-intuitif. De plus, dans tous les cas le facteur humain reste un gros problème.

Donc, est-ce que goto est vraiment malveillant ? Non, mais il reste, dans l’absolu, une très mauvaise pratique.

À propos des exceptions

Les exceptions : une manière moderne de casser les structures de contrôle

Les exceptions sont une manière de gérer les cas d’erreur. Elles peuvent aussi être utilisées comme une structure de contrôle standard, vu qu’on peut personnaliser ses propres exceptions, puis les lever et les attraper à loisir.

J’aime bien m’imaginer les exceptions comme étant des if qui pendouillent dans le code : d’une part si tout se passe bien, le code se déroule de manière linéaire, sans accroc, mais d’autre part, si une erreur survient, alors on lance la balle dans les airs en espérant que quelque chose dans les strates supérieures du programme la rattrape.

Les exceptions cassent les structurent de contrôle standard. Reprenons l’image de la corde déroulée le long du code : quand vous appelez une fonction qui peut lever une exception, alors un brin de corde se détache pour exécuter la fonction (comme avant), mais vous n’avez aucune garantie que le brin sera retourné à votre corde à l’endroit où vous avez appelé la fonction. Il pourra être raccordé n’importe où au-dessus, dans la pile d’appel. Le seul moyen d’empêcher ce scénario est de trycatch toutes les exceptions quand vous appelez la fonction, mais c’est un luxe qui n’est possible que si vous savez quoi faire dans tous les cas dégradés possibles.

De plus, quand vous écrivez une fonction dans laquelle vous levez possiblement une exception, vous n’avez aucun moyen de savoir si et où elle sera attrapée.

Même si ce n’est pas aussi mauvais que goto, parce qu’on a plus de contrôle dessus, il est très facile d’écrire du code spaghetti en usant d’exceptions. Avec ce recul, on peut même considérer que les exceptions sont « des goto modernes ».

On peut même écrire l’exemple des boucles imbriquées avec des exceptions :

//...
 
try
{
    for (int i = 0 ; i < size_i ; ++i)
    {
        for (int j = 0 ; j < size_j ; ++j)
        {
            if (condition_of_exit(i,j))
                throw;
        }
    }
}
catch (const std::exception& e)
{ /* nothing */ }
 
//...

Je ne recommanderais pas une telle écriture cependant. Les exceptions sont malfaisantes.

Est-ce que les exceptions sont vraiment malfaisantes ?

… le sont-elles ? Pas vraiment.

Dans la section précédente, j’ai statué sur le fait que les goto ne sont pas à proprement parler malveillantes mais qu’elles sont très sujettes aux fautes et aux erreurs. Elles n’en valent juste pas la peine. C’est pareil pour les exceptions : elles ne sont pas malfaisantes, c’est juste une feature.

En mon humble opinion, contrairement aux goto, il existe des moyens sécurisés d’utiliser les exceptions.

Une différence majeure entre les exceptions et les goto

Pour comprendre comment bien utiliser les exceptions, il faut comprendre les différences qu’elles ont avec le goto.Les exceptions, contrairement aux goto, n’envoient pas l’exécution à un endroit totalement indéterminé, elle envoie l’exécution vers le haut. Cela peut être de quelques blocs (comme dans l’exemple des boucles imbriquées) ou sur plusieurs appels de fonctions.

Quand utiliser les exceptions ?

Envoyer l’exécution du programme vers le haut reste assez indéterminé malgré tout, et dans la plupart des cas cela brise la continuité du programme.

Dans la plupart des cas.

Il existe une situation spécifique où vous voulez que l’exécution se stoppe abruptement : quand quelqu’un utilise une fonctionnalité d’une manière qui provoque un comportement indésirable.

Quand vous écrivez une fonctionnalité, vous voulez avoir la possibilité de tout stopper pour éviter un cas dégradé dangereux, et à la place de finir le traitement renvoyer l’exécution vers le haut en indiquant « Quelque chose d’imprévu s’est produit, je ne peux pas continuer ». Non seulement cela va prévenir les comportements indésirables, mais cela va aussi indiquer à l’utilisateur qu’il n’a pas correctement utilisé la fonctionnalité, le forçant à se corriger ou à gérer le cas d’erreur.

Quand vous rédigez une fonctionnalité, il y a un mur virtuel entre vous et l’utilisateur, avec un petit trou symbolisé par l’interface de la fonctionnalité. C’est à chacun d’entre vous de gérer correctement le comportement de la corde proverbiale de chaque côté du mur.

Une fonctionnalité peut d’ailleurs très bien être une grosse librairie ou une simple classe. Du moment qu’il y a un niveau d’encapsulation, il est valide d’utiliser des exceptions comme faisant partie de l’interface.

Un bon exemple de cela est la méthode at () de std ::vector < >. Le but de cette méthode est de renvoyer le énième élément du vecteur, mais il y a un risque que l’utilisateur demande un élément hors-limite. Dans ce cas, lever une exception est le moyen qu’a std ::vector < > d’empêcher un comportement indéfini. Si l’utilisateur capture l’exception, alors il peut écrire le code à exécuter en cas d’indice hors-limite. Sinon, le programme sera stoppé, lui indiquant qu’il a commis une erreur et le forcera à sécuriser son code ou à gérer le cas dégradé.

Dans tous les cas, vous devez documenter toute exception que vous levez.

Conclusion

En résumé, les bonnes pratiques à employer vis-à-vis des exceptions peuvent se résumer en trois points :

  • N’utilisez pas les exceptions comme des structures de contrôle.
  • Vous pouvez utiliser les exceptions comme partie de l’interface d’une fonctionnalité.
  • Vous devez documenter toute exception que vous levez.

Merci de votre attention et à la semaine prochaine !

Article original : Exceptions are just fancy gotos | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Comment gérer le dépassement d’entier

Article original : Dealing with integer overflows | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Le dépassement d’entier (integer overflow dans la langue de Ritchie) peuvent être considérablement embêtants. Il n’y a pas de manière simple et fiable de les détecter, encore moins de les détecter statiquement, et ils peuvent rendre votre logiciel inconsistant.

Cet article parlera indistinctement du dépassement des entiers signés et non-signés. Même si le dépassement des entiers non-signés n’est pas un comportement indéfini (contrairement au dépassement des entiers signés), 99.9% du temps vous ne voulez ni l’un, ni l’autre.

Comment détecter les dépassements d’entier ?

Même si elle sont peu nombreuses, il existe effectivement des manières de détecter les dépassements d’entier. Le souci est qu’elles sont soit complexes, soit peu fiables.

Dans cette section, je vais vous présenter quelques manières de détecter les dépassements d’entier (si vous en connaissez d’autres, n’hésitez pas à le partager en commentaire pour que tout le monde puisse en profiter)

UBSan

Undefined Behavior Sanitizer, ou UBSan pour les intimes, est un outil dynamique de vérification des comportements indéfinis.

Il est capable de détecter les dépassements d’entier sous la forme d’une option de compilation. Notez que même s’il est conçu pour détecter les comportements indéfinis, il nous fait le plaisir de détecter également les dépassements de entiers non-signés (qui, pour rappel, ne sont pas indéfinis).

Voici, à titre d’exemple, lesdites options de compilations appliquée au compilateur clang :

clang++ -fsanitize=signed-integer-overflow -fsanitize=unsigned-integer-overflow

Cet outil est implémenté pour clang et GCC :

Je peux me tromper, mais je n’ai pas réussi à trouver d’intégration de cet outil sur d’autres compilateurs.

Le principal inconvénient d’un outil dynamique comme celui-ci est qu’il faut re-compiler et lancer des essais exhaustifs du logiciel pour que l’outil puisse détecter un éventuel problème. Si les essais ne couvrent pas assez de cas de figure, il est possible qu’un dépassement d’entier passe entre les mailles du filet.

Écrire des test unitaires adéquats

Si votre projet implémente des tests unitaires (je ne m’étendrais pas sur le fait qu’il le devrait, car les développeurs ne sont pas toujours décisionnaires sur ce sujet), alors vous avez à votre disposition une manière assez directe de prévenir les dépassements d’entier.

Il vous suffit, pour toute fonctionnalité où c’est pertinent, de fournir en entrée de cette fonctionnalité de très grands entiers. Vous n’avez plus qu’à vérifier que le résultat reste cohérent, qu’une exception est levée, qu’un code d’erreur est mis à jour, ou quoique ce soit que la fonctionnalité est sensée faire dans ce cas.

Si vous ne savez pas quel résultat la fonctionnalité est sensée renvoyer parce que l’entrée est trop grosse pour être traitée et qu’elle ne gère pas ces cas d’erreur, alors cette fonctionnalité est dangereuse. Vous devez implémenter les cas d’erreur pour vous assurer qu’aucun dépassement n’aura lieu.

Parfois, détecter un dépassement d’entier potentiel peut nécessiter un refactoring assez lourd. N’ayez pas peur de le faire : il vaut mieux prévenir que guérir.

Ne mettez pas votre code dans une situation où il pourrait faire un dépassement

Le meilleur moyen d’être sûr qu’il n’y aura pas de dépassement et de prévenir toute possibilité qu’il survienne. Si vous être certain que votre code ne peux pas provoquer de dépassement d’entier, vous n’aurez pas besoin de les détecter.

La section suivante va vous fournir quelques pratiques pour vous aider dans cette optique.

Comment prévenir le dépassement d’entier ?

Utilisez des entiers de 64 bits

Une très bonne manière d’éviter le dépassement est d’utiliser int64_t pour implémenter les entiers. Dans la plupart des cas, les entiers de 64 bits ne provoqueront pas de dépassement, contrairement à leurs homologues de 32 bits et moins.

Veuillez noter que j’utilise des entiers signés comme exemple mais que toute cette section vaut aussi pour les entiers non-signés.

Il y a en réalité très peu de désavantages à utiliser int64_t plutôt que int32_t. La plupart du temps, vous n’aurez pas besoin de vous inquiéter de l’écart de performance ou de l’écart de taille entre les deux types. Seulement si vous travaillez sur des système embarqués ou sur des algorithmes de traitement de données pour pourriez avoir à vous en soucier.

Notez que avoir des entiers plus grands ne veut pas nécessairement dire que les calculs seront plus longs, et avec tout le panel de pointeurs / références / forwarding que le C++ nous fournit, on n’a pas souvent besoin de copier des grosses structure de données.

Dans tous les cas, même si vous avez des contraintes de performance ou de taille, gardez en tête la maxime suivante :

D’abbord la sécurité, ensuite les performances.
– Chloé Lourseyre, 2021

Essayer de suroptimiser et risquer ce faisant un dépassement d’entier est toujours pire que d’écrire du code sécurisé puis d’utiliser des outils pour cibler les morceaux de code qui doivent être optimisés.

Donc, ma recommandation finale est que, sauf contre-indication spécifique, vous utilisiez des int64_t (et des uint64_t) pour coder vos entiers.

À propos des performances de int64_t

J’ai fait tourner quelques opérations arithmétiques (addition, multiplication et division) sur un outil de benchmarking avec à la fois des int32_t et des int64_t pour voir s’il y avait des différences notables.

En utilisant clang et sur deux niveaux d’optimisation différents, voici les résultats :

Cela ne vous surprendra peut-être pas, mais dans tous les cas c’est la division la plus lente. Nonobstant les divisions, vous noterez qu’il n’y a pas de différence sensible entre les entiers 64-bits et les entiers 32-bits. Par contre, si vous effectuez des divisions, les entiers 32-bits seront plus adaptés (mais le simple fait d’utiliser des divisions démoli vos performance, donc c’est un gain en demi-teinte).

Petit rappel sur les types de base

Peut-être vous êtes vous demandé pourquoi, depuis le début de cette section, j’utilise les types int32_t et int64_t et non les bon vieux int et long. La raison à cela, c’est tout simplement parce que la taille de ces deux types dépend de votre environnement.

En effet, les seules contraintes que le standard applique sur les tailles des int et long sont les suivantes :

  • Les int doivent faire au moins 16-bits.
  • Les long doivent faire au moins 32-bits.
  • La tailles des int doit être supérieure ou égale à la taille des short et inférieure ou égale à la taille des long.
  • La taille des long doit être supérieure ou égale à la taille des intbool et wchar_t et inférieure ou égale à la taille des long long.

À cause de cela, évitez autant que possible d’utiliser les int et les long quand vous voulez éviter les dépassements d’entier.

Ne présumez jamais que, parce qu’une valeur est dans les limites, elle ne provoquera pas de dépassement d’entier

Mettons que vous avez une variable int32_t my_val qui représente un donnée qui sera toujours contenue entre 0 et un milliard (1 000 000 000). Comme la valeur max d’un int32_t est 2^31-1 (2 147 483 647), on pourrait penser que la variable ne provoquera jamais de dépassement.

Mais, un jour fatidique, un développeur écrira, sans rien soupçonner :

#include <cstdlib>
 
const int32_t C_FOO_FACTOR = 3;
 
int32_t evaluate_foo(int32_t my_val)
{
    // foo is always C_FOO_FACTOR times my_val
    return my_val * C_FOO_FACTOR;
}

Vous l’avez vu ? Dépassement d’entier. En effet, il existe des valeur de my_val qui, quand elles sont multipliées par 3, causent un dépassement d’entier.

À qui la faute ? Doit-on toujours vérifier qu’une valeur est dans un intervalle adéquat avant toute opération arithmétique ? Comment faire ?

Et bien, il y a une pratique assez simple à réaliser qui vous aidera à prévenir la plupart des cas de figure concernés. Quand vous aurez à stocker un entier plutôt grand, même si en lui-même il ne peut pas dépasser, mettez-le dans une structure plus grande.

Par exemple, je ne met jamais une valeur qui peut être plus grande que 2^15 dans une donnée dont la valeur maximale est de 2^31. Ainsi, même si la valeur est multipliée avec elle-même, elle ne provoquera pas de dépassement d’entier.

Avec cette méthode, on peut garder les petites valeurs dans de plus petites structures de donnée sans effet de bord. Dans l’exemple mentionné, C_FOO_FACTOR pourra rester un int32_t, et le résultat de toute opération avec lui sera promu s’il est associé avec un type de plus grande taille dans une opération arithmétique.

E.g. :

#include <cstdlib>
 
const int32_t C_FOO_FACTOR = 3;
 
int64_t evaluate_foo(int64_t my_val)
{
    // foo is always C_FOO_FACTOR times my_val
    return my_val * C_FOO_FACTOR; // The result of the multiplication is a int64_t
}

Utilisez auto

Oui, auto peut parfois vous sauver la vie, en particulier si vous n’êtes pas 100% sûr des type que vous manipulez.

Par exemple :

#include <cstdlib>
 
int32_t  C_FOO = 42;
 
int64_t compute_bar();
 
int main()
{
    // Big risk of overflow overflow here
    int32_t foo_bar = compute_bar() + C_FOO;
 
    // Probably no overflow here
    auto better_foo_bar = compute_bar() + C_FOO;
}

Ici, auto est utile car il prévient l’erreur commise ligne 10, où le résultat de l’opération compute_bar() + C_FOO, qui est un int64_t, est rétrogradée en int32_t quand elle est assignée. Ligne 13, auto devient un int64_t, donc aucun dépassement ne peut avoir lieu.

(Note : convertir un entier en un type trop petit pour le contenir est un dépassement d’entier)

Il y a aussi un autre cas spécifique, qui ne survient pas spécialement souvent, où auto peut être utile. Considerez le code suivant :

#include <cstdlib>
 
int32_t  C_FOO = 42;
 
int32_t compute_bar();
 
int main()
{
    // Big risk of overflow overflow here
    auto foo_bar = compute_bar() + C_FOO;
}

Ici, la valeur de retour de compute_bar() est int32_t. Mais si plus tard, l’auteur de cette fonction, voyant que int32_t est trop petit, change le type de retour en un int64_t, comme ceci :

#include <cstdlib>
 
int32_t  C_FOO = 42;
 
int64_t compute_bar();
 
int main()
{
    // Big risk of overflow overflow here
    auto foo_bar = compute_bar() + C_FOO;
}

Alors l’auto sera automatiquement « promu » en int64_t lui aussi, évitant une conversion implicite qui pourrait résulter en un dépassement d’entier.

Si on avait utilisé int32_t à la place de auto dès le départ, alors il y aurait eu un risque que l’auteur de la fonction compute_bar() ne corrige pas le type de la variable foo_bar, sans qu’aucun avertissement ni aucune erreur ne survienne à la compilation. Du coup, avoir auto dans ce cas nous a fait éviter le pire.

En conclusion

Faites toujours attention, quand vous manipulez des grands entiers, d’utiliser des grands type (bien plus grand que nécessaire, par sécurité). Utilisez auto quand vous ne savez pas ce que vous manipulez, et utilisez des analyseurs si vous pensez que votre code peut contenir un dépassement d’entier. Et bien sûr, comme toujours, écrivez de bons tests unitaires.

Si vous connaissez d’autres manières de prévenir ou de détecter des dépassement d’entier, n’héistez pas à le partager en commentaire.

Merci de votre attention et à la semaine prochaine !

Article original : Dealing with integer overflows | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Une liste de mauvaises pratiques couramment rencontrées dans le développement de logiciels industriels

Article original : A list of bad practices commonly seen in industrial projects | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Sur beaucoup de projets C++ industriel, il est très courant de voir un nombre conséquents de développeurs partager une même codebase.

Aujourd’hui, une grande majorité des développeurs C++ ne sont pas des experts du langage. Il y en a beaucoup qui sont issus d’autres langages avec d’autres pratiques, des gens qui ont seulement appris le C++ à l’école et qui ne sont pas spécialement intéressés par les mécaniques spécifiques au langage et qui ont des spécialités de plus haut niveau, ou encore des développeurs issus des temps anciens qui codent encore en « C avec des classes ».

L’idée de cet article est de lister une série (non-exhaustive) de mauvaises pratiques plutôt répandues afin que chacun, que que soit son niveau, puisse contribuer à une codebase plus saine et plus maintenable.

Mauvaise pratique : les fonctions trop longues

Je ne suis pas homme (ni femme) à imposer des limites strictes pour la longueur des fonctions. Cependant, quand on commence à avoir des fonctions qui font plusieurs milliers, voire plusieurs dizaines de milliers de lignes de code, il est nécessaire de mettre le holà.

Une fonction (comme tout bloc) est une unité architecturale du code. Plus elle est longue, plus elle est difficile à assimiler dans son entièreté. Si elle est divisée en unités plus petites, chaque unité est individuellement plus aisée à comprendre car l’esprit est tour-à-tour focalisé sur des problèmes de plus petite taille. Il suffit ensuite pour lui de les assembler pour comprendre la fonction dans sa globalité.

En d’autre terme, pour comprendre une grosse problématique, il est plus simple de la diviser en problématiques plus petites.

Ainsi, vos fonctions serviront parfois juste à appeler des fonctions auxiliaires en succession, ce qui est pratique car cela permet d’avoir le cheminement fonctionnel de la fonction sans avoir à se plonger dans le détail des implémentations.

La limite que je me fixe personnellement tourne autour de 200 lignes par fonction (parfois plus, parfois moins).

Mauvaise pratique : créer des classes là où il n’y en a pas besoin.

C’est quelque chose qui est surprenamment assez commun, de ce que j’ai pu en voir, probablement dû à des pratiques importées d’autres langages où la notion de classes est perçue différemment.

Il y a deux manières sous lesquelles cette mauvaise pratique peut survenir :

Les classes complètement statiques (avec parfois des constructeurs)

Il est plus simple d’illustrer cela avec un exemple, alors allons-y :

class MyMath
{
public:
    MyMath();
    ~MyMath();
    static int square(int i);
};
 
MyMath::MyMath()
{
}
 
MyMath::~MyMath()
{
}
 
int MyMath::square(int i)
{
    return i*i;
}
 
int main()
{
    MyMath mm;
    int j = mm.square(3);
    return j;
}

Voici les points problématiques avec ce code :

  • Pourquoi implémenterait-on un constructeur et un destructeur vide, alors que ceux par défaut feraient très bien l’affaire ?
  • Pourquoi implémenterait-on un constructeur et un destructeur dans une classe entièrement statique ?
  • Pourquoi créerait-in une instance de cette classe juste pour appeler la méthode statique ?
  • Pourquoi créerait-on une classe pour faire cela alors qu’un namespace ferait complètement l’affaire ?

Voici à quoi ce code devrait plutôt ressembler :

namespace MyMath
{
    int square(int i);
};
 
int MyMath::square(int i)
{
    return i*i;
}
 
int main()
{
    int j = MyMath::square(3);
    return j;
}

Plus concis, plus propre, meilleur sous tout rapport.

Certes, parfois les classes complètement statiques peuvent être utiles, mais dans les situations similaires à cette exemple, elles ne le sont pas.

Il n’y a aucun intérêt à utiliser une classe alors qu’on pourrait ne pas le faire. Si vous pensez que le namespace pourrait avoir besoin d’être transformé en classe dans le futur (avec des attributs et des méthodes), souvenez vous juste de ce petit dicton :

Ne codez jamais en prévision d’un futur hypothétique qui pourrait ou ne pourrait pas se réaliser. Le temps que vous passez à être trop prévoyant pour le futur est du temps perdu. Vous pourrez toujours refactorer plus tard au besoin.

Les classes complètement transparentes

J’ai mis ce sujet en second car il est beaucoup plus sujet à controverse que le premier.

Juste pour être clair : la seule différence entre une class et une struct, c’est que les membres d’une class sont privés par défaut, alors que les membres d’une struct sont publiques par défaut. C’est la seule différence.

Du coup, si votre classe :

  • … n’a que des méthode publiques
  • … a des accesseurs (get/set) pour tous ses attributs
  • … n’a que des accesseur simplistes

… alors ce n’est pas une classe, c’est une structure.

Voici un exemple d’illustration :

class MyClass
{
    int m_foo;
    int m_bar;
 
public:
    int addAll();
    int getFoo() const;
    void setFoo(int foo);
    int getBar() const;
    void setBar(int bar);
};
 
int MyClass::addAll()
{ 
    return m_foo + m_bar;
}
int MyClass::getFoo() const
{
    return m_foo;
}
void MyClass::setFoo(int foo)
{
    m_foo = foo;
}
int MyClass::getBar() const
{
    return m_bar;
}
void MyClass::setBar(int bar)
{
    m_bar = bar;
}

Ce code peut être écrit beaucoup plus simplement avec une structure :

struct MyClass
{
    int foo;
    int bar;
 
    int addAll();
};
 
int MyClass::addAll()
{ 
    return foo + bar;
}

Ces deux codes sont assez équivalent, la seule réelle différence étant que la classe fournit un degré d’encapsulation supplémentaires (et inutile).

La controverse vient du caractère « inutile » que je viens de mentionner. En effet, dans une mentalité complètement orienté-objet, aucune encapsulation n’est inutile. De mon humble avis, ce genre de structure ne nécessite pas d’encapsulation car elle représente un ensemble de données stockées telles quelles, et je pense qu’il est contre-productif d’essayer de trop encapsuler le code de manière générale.

Attention cependant, car la pratique que je décrit ici n’est valide que si tout les attributs sont en accès direct à la fois en lecture et en écriture. Si vous avez des accesseurs spécifiques ou des attributs en lecture-seule ou écriture-seule, n’utilisez pas une struct (enfin, vous pouvez mais il faudra que vous y réfléchissiez sérieusement).

Mauvaise pratique : implémenter un comportement indéfini

Les undefined behavior, aussi appelés UB ou encore « comportements indéfinis » sont, pour faire court, un contrat que le développeur passe avec le compilateur, promettant à celui-ci qu’il n’implémentera pas certains comportements. Cela permet au compilateur de faire des hypothèses en prenant cela en compte et de faire des optimisations en conséquence.

Allez voir la conférence de Piotr Padlewski si vous voulez plus de détails à ce propos : CppCon 2017: Piotr Padlewski “Undefined Behaviour is awesome!” – YouTube.

Voici une liste non-exhaustive des UB que vous devez absolument connaître pour éviter d’avoir des comportements indéfinis à votre insu dans votre codebase :

  • Appeler la fonction main()
  • Le dépassement d’entier
  • Le dépassement de buffer
  • Utiliser des variables non-initialisées
  • Déréférencer nullptr
  • Omettre l’instruction return
  • Nommer une variable en commençant avec deux underscores __
  • Définir une fonction dans le namespace std
  • Spécialiser un type qui n’a pas été défini par l’utilisateur dans le namespace std
  • Prendre l’adresse d’une fonction de la std

En conséquence, et que ce soit dit une bonne fois pour toute : n’utilisez jamais, au grand jamais le dépassement d’entier comme condition de sortie d’une boucle ou pour toute autre raison. Parce que cela ne fonctionne pas comme vous le pensez et qu’un jour ça va se retourner contre vous.

Mauvaise pratique : comparer un entier signé avec un entier non-signé

Quand vous comparez un entier signé avec un non-signé, il y aura forcement une conversion arithmétique des types qui a une chance non-négligeable de fausser les valeurs et rendre caduque la comparaison.

Utilisez le type size_t quand c’est pertinent, et appliquez une static_cast sur vos données quand c’est nécessaire.

Mauvaise pratique : essayer d’optimiser le code tout en l’écrivant

Oui, c’est peut-être dur à avaler, mais voici deux faits :

  • 80% du temps (voire même plus), le code que vous écrivez n’a pas besoin d’être optimisé. Puisque la plupart du temps d’exécution su programme survient dans 20% du code (principe de Pareto à l’œuvre), les 80% restants n’ont pas besoin d’être optimisés.
  • L’optimisation ne doit pas être une préoccupation a priori. Ce que vous devez faire est : écrire votre code, prendre du recul et optimiser en conséquence.

La chose la plus importante à faire n’est pas d’optimiser votre programme, mais de vous arranger avant tout pour que votre code soit concis, élégant et maintenable. Si il l’est, vous pourrez toujours revenir plus tard pour optimiser si vous en avez effectivement besoin.

Mauvaise pratique : ne pas assez réfléchir

Certes, vous ne devez pas spécialement chercher à optimiser en écrivant du code, mais vous ne devez pas non plus tomber dans l’extrême inverse, c’est à dire sous-optimiser votre code à mesure que vous l’écrivez.

Connaissez les spécificités des algorithmes que vous utilisez, soyez conscients des buts des différentes structures de données et à quels besoins elles répondent, connaissez vos design pattern et soyez capables de les implémentez, et n’hésitez surtout pas à lire la doc des features que vous comptez utiliser.

Il y a un bon équilibre à atteindre entre coder sans réfléchir et la sur-optimisation.

Mauvaise pratique : « il faut faire ça parce qu’on en aura besoin plus tard… »

Je l’ai déjà dit et je le répète : ne codez pas en fonction d’un futur hypothétique, car tout peut changer au cours du développement. La seule chose qui est réellement inamovible, c’est le principe fondamental de votre programme, et rien de plus.

Vos besoins peuvent changer, vos specs peuvent changer, ce que le client demande peut changer, vos procédures peuvent changer, tout ce qui n’est pas le principe de base de votre programme peut changer. Parfois ça ne changera pas, mais souvent ces notions vont evoluer.

Si vous êtes en proie au doute, demandez-vous simplement :

Si j’implémente ça plus tard, est-ce que ça sera plus coûteux ?

Souvent, la réponse sera « non » ou « pas tant que ça ». Dans ce cas, il vaut mieux laisser le futur au futur.

En conclusion…

Là, vous avez un bon point de départ pour écrire du code élégant et surtout maintenable. Si tous les développeurs de votre projet appliquent ces règles, cela facilitera la vie de tout le monde.

Cependant il y a beaucoup d’autres mauvaises pratiques qui existent et ne sont pas listées ici. Peut-être publierai-je un autre article parlant de cela, peut-être pas.

Merci de votre attention et à la semaine prochaine !

Article original : A list of bad practices commonly seen in industrial projects | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Lier une lambda à une référence constante

Article original : Yet another pamphlet about inlining | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Contexte

Cette semaine, laissez-moi vous présenter un morceau de code que j’ai pu voir il n’y a pas si longtemps :

#include <vector>
#include <algorithm>
 
int count_even(const std::vector<int>& v)
{
    const auto& my_func = [] (int i)->bool
    {
        return i%2==0; 
    };
 
    return std::count_if(std::cbegin(v), std::cend(v), my_func);
}

En voyant cela, VisualStudio n’était pas certain si cela devait compiler ou pas.

En fait, au cours de la compilation, on pouvait voir une erreur du style « my_func is used but already destroyed » apparaître et disparaître périodiquement, puis disparaître complètement à la fin de la compilation.

Quand j’ai vu tout cela, je me suis dit deux choses :

« Attendez, on peut lier une variable temporaire à une référence constante ? »

et

« Quel est l’intérêt de lier une lambda à une référence constante ? »

C’est à ces deux questions que nous allons répondre aujourd’hui.

Lier une rvalue à une référence constante

Pour la faire courte, oui, on peut lier une un rvalue à une référence constante.

Intuitivement, j’aurais au départ dit que faire cela ne résulterait qu’en une dangling reference, mais ce n’est pas le cas.

Mais en y repensant, c’est plutôt logique, car le langage est pensé de manière intelligente et lorsque vous liez une référence constante à un objet temporaire, l’objet sera mis sur la pile mémoire et vous pourrez toujours y accéder, tant que cette référence constante existera.

Pour illustrer cela, voici deux fonctions :

void foo()
{
    const int& i = 1;
    const int& j = i+1;
}
 
void bar()
{
    int iv = 1;
    const int& i = iv;
    int jv = i+1;
    const int& j = jv;
}

La fonction foo lie directement les rvalues à des références constantes, alors que bar les lient d’abord à des variables avant de lier ces variables (qui sont des lvalues) à des références constantes.

Si on regarde le code assembleur généré par ce code (avec clang 11.0 et -O3), on obtient :

foo():
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 12], 1
        lea     rax, [rbp - 12]
        mov     qword ptr [rbp - 8], rax
        mov     rax, qword ptr [rbp - 8]
        mov     eax, dword ptr [rax]
        add     eax, 1
        mov     dword ptr [rbp - 28], eax
        lea     rax, [rbp - 28]
        mov     qword ptr [rbp - 24], rax
        pop     rbp
        ret
bar():
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 4], 1
        lea     rax, [rbp - 4]
        mov     qword ptr [rbp - 16], rax
        mov     rax, qword ptr [rbp - 16]
        mov     eax, dword ptr [rax]
        add     eax, 1
        mov     dword ptr [rbp - 20], eax
        lea     rax, [rbp - 20]
        mov     qword ptr [rbp - 32], rax
        pop     rbp
        ret

Nonobstant l’alignement sur la pile, ces deux fonctions sont exactement identiques. Ce fait est confirmé par la documentation IBM : Initialization of references (C++ only) – IBM Documentation et, bien entendu, par le standard (The C++11 Programming Language, §7.7.1)

Il s’agit, au final, d’une pratique assez simple à comprendre mais très peu usitée en pratique, et qui est peu référencée sur le web.

La raison à cela est que lier une temporaire à une référence constante au lieu de simplement la lier à une valeur constante – ou même une valeur tout court – à l’air assez inutile en pratique.

Mais l’est-ce réellement ?

Lier une lambda à une référence constante

Pour revenir un peu au contexte initial, la question était : pourquoi lier une lambda à une référence constante est légal et est-ce utile ?

En tant que rappel, revoici l’exemple :

#include <vector>
#include <algorithm>
 
int count_even(const std::vector<int>& v)
{
    const auto& my_func = [] (int i)->bool
    {
        return i%2==0; 
    };
 
    return std::count_if(std::cbegin(v), std::cend(v), my_func);
}

Si on met ce code dans C++ Insights (cppinsights.io), on obtient le code suivant :

#include <vector>
#include <algorithm>
 
int count_even(const std::vector<int, std::allocator<int> > & v)
{
     
  class __lambda_6_27
  {
    public: 
    inline /*constexpr */ bool operator()(int i) const
    {
      return (i % 2) == 0;
    }
     
    using retType_6_27 = auto (*)(int) -> bool;
    inline /*constexpr */ operator retType_6_27 () const noexcept
    {
      return __invoke;
    };
     
    private: 
    static inline bool __invoke(int i)
    {
      return (i % 2) == 0;
    }
     
    public: 
    // inline /*constexpr */ __lambda_6_27(const __lambda_6_27 &) noexcept = default;
    // inline /*constexpr */ __lambda_6_27(__lambda_6_27 &&) noexcept = default;
    // /*constexpr */ __lambda_6_27() = default;
     
  };
   
  const __lambda_6_27 & my_func = __lambda_6_27{};
  return static_cast<int>(std::count_if(std::cbegin(v), std::cend(v), __lambda_6_27(my_func)));
}

Comme vous pouvez le voir (et vous l’aviez peut-être déjà deviné), une lambda fonction est en réalité un foncteur (ici appelé __lambda_6_27). De ce fait, quand on assigne une lambda à une variable, on appelle en réalité le constructeur du foncteur correspondant. Or, la valeur retournée par cet appel passe nécessairement par une variable temporaire dans ce contexte, ce qui est une rvalue.

Et comme nous l’avons déjà vu, il est légal d’assigner une value à une référence constante.

C’est pour cela que lier une lambda a une référence constante est légal.

Performance et optimisation

Après avoir vu que l’on pouvait lier une lambda à une référence constante, essayons de savoir si on devrait le faire.

Pour cela, analysons les performances en temps d’exécution et de compilation de chaque possibilité.

Temps d’exécution

J’ai utilisé Quick C++ Benchmarks (quick-bench.com) pour benchmarker le temps d’exécution du morceau de code suivant :

std::vector<int> v = {0,1,2,3,4};
 
static void ConstRef(benchmark::State& state)
{
  const auto& l = [](int i)->bool{ return i%2 == 0;};
  for (auto _ : state)
  {
    std::count_if(cbegin(v), cend(v), l);
  }
}
BENCHMARK(ConstRef);
 
static void Plain(benchmark::State& state)
{
  auto l = [](int i)->bool{ return i%2 == 0;};
  for (auto _ : state)
  {
    std::count_if(cbegin(v), cend(v), l);
  }
}
BENCHMARK(Plain);

J’ai effectué le benchmarking sur clang 11.0 et GCC 10.2, avec chaque option d’optimisation.

Voici les résultats :

CompilateurOption
d’optimisation
TE avec
const ref
TE avec
valeur brute
ratio const ref /
valeur brute
Clang 11.0-O032.30633.7320.958
Clang 11.0 -O1224.96204.921.097
Clang 11.0 -O23.9982e-64.0088e-60.997
Clang 11.0 -O33.7273e-64.1281e-60.903
GCC 10.2-O0 64.37965.0170.990
GCC 10.2 -O1 11.75411.8710.990
GCC 10.2 -O2 3.7470e-64.0196e-60.932
GCC 10.2 -O3 3.6523e-63.9021e-60.936

La colonne qui nous intéresse est la dernière, qui calcule le rapport de temps d’exécution entre la version liant une référence constante et la version liant une valeur (> 1 signifie que la version liant une référence constante est plus lente que l’autre).

Voici les graphiques associés :

En tout et pour tout on peut voir que même si la version liant un référence constante est globalement meilleure que la version liant une valeur, la différence est toujours inférieure à 10%.

Si le code est dans un goulot d’étranglement (i.e. dans les 20% du principe de Pareto), alors ces 10% peuvent éventuellement faire la différence, mais cela ne permettra jamais d’avoir plus de deux ou trois pourcents de gain dans la globalité d’un code plus massif.

Par contre, si le code n’est pas dans un goulot d’étranglement, il n’y a dans les faits aucune différence notable entre les deux version.

Temps de compilation

Est-ce que lier une lambda à une référence constante affecte le temps de compilation ? Pour répondre à cela, j’ai utilisé C++ Build Benchmarks (build-bench.com)1

Voici les deux codes que j’ai lancé séparément pour les comparer :

#include <vector>
 
int main() 
{
    const auto& l = [](int i)->bool{ return i%2 == 0;};
    std::vector<int> v= {0,1,2,3,4};
    std::count_if(cbegin(v), cend(v), l);
}

et

#include <vector>
 
int main() 
{
    auto l = [](int i)->bool{ return i%2 == 0;};
    std::vector<int> v= {0,1,2,3,4};
    std::count_if(cbegin(v), cend(v), l);
}

Et voici les résultats :

CompilateurOption
d’optimisation
TB avec
const ref
TE avec
valeur brute
ratio const ref /
valeur brute
Clang 11.0-O00.36290.35101.034
Clang 11.0 -O10.40100.40350.994
Clang 11.0 -O20.37550.37650.997
Clang 11.0 -O30.37450.37351.003
GCC 10.2 -O0 0.39150.39001.004
GCC 10.2 -O1 0.38300.38101.005
GCC 10.2 -O2 0.37650.37750.997
GCC 10.2 -O3 0.37650.37501.004

Dans tous les cas, on observe une différence inférieures à 4%, et même dans la plupart des cas inférieure à 1%. On peut dire sans risque qu’il n’y a pas de différence significative entre les deux versions.

Conclusion

Il n’y a pas de réel avantage significatif a lier une lambda (ou plus généralement une rvalue) à une référence constante plutôt qu’à une valeur. La plupart du temps, vous préférerez utiliser une simple valeur ou une valeur constante, qui a le bénéfice de ne pas trop alourdir l’écriture avec un symbole supplémentaire.

Dans le cas où vous vous trouvez dans un goulot d’étranglement, vous pourriez considérer l’utilisation d’une référence constante en vue d’optimiser un peu le temps d’exécution, mais dans ce cas là je vous suggère de faire vos propre benchmarks pour déterminer si, dans votre contexte spécifique, c’est effectivement utile.

Merci de votre attention et à la semaine prochaine.

Article original : Yet another pamphlet about inlining | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Encore un pamphlet sur `inline`

Article original : Yet another pamphlet about inlining | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Introduction

Le sujet de cette semaine sera le mot-clé inline.

C’est un sujet de longue date, qui pour moi est clos depuis longtemps. À chaque fois que je le rouvre pour me re-documenter à son sujet, j’en arrive aux mêmes conclusions.

Cela dit, je lis très souvent du code qui utilise mal ce mot-clé, ce qui finit toujours par des situations dangereuses et contre-productives.

Je vais essayer ici de résumer tout ce qu’il faut savoir à propos de inline, les avantages, les inconvénients et lister les situations où il faut ou ne faut pas l’utiliser.

Définition

Je vais commencer par une définition académique donnée par inline specifier – cppreference.com et traduite par mes soins:

Le but originel du mot-clé inline était de servir d’indicateur pour l’optimiseur que la substitution du corps de la fonction était préférable à son appel, c’est-à-dire au lieu d’exécuter l’instruction CPU d’appel de la fonction pour transférer le contrôle au corps de la fonction, une copie de ce corps de fonction est créé par l’appel de fonction sans générer effectivement d’appel. Cela permet d’éviter les instructions supplémentaires créées par l’appel de fonction (passer les argument et retourner le résultat) mais peut avoir pour conséquences un plus gros exécutable, vu que le corps de la fonction sera copié plusieurs fois.

Étant donné que ce mot-clé n’implique pas de contrainte forte, les compilateurs sont libres de substituer le corps de la fonction là où le mot-clé inline n’est pas précisé et de générer des appels de fonction là où le mot-clé inline est précisé. Ces choix d’optimisation ne changent pas les règles à propos des multiples définitions.

Il faut retenir deux choses de cette définition :

  • Le mot-clé inline permet d’éviter des instructions assembleur inutiles, remplaçant l’appel d’une fonction par directement le code qu’elle implémente.
  • Il s’agit simplement d’une indication donnée au compilateur, qu’il est libre de respecter ou non.

Avec cela en tête, faisons un petit tour des avantages et des inconvénients.

Avantages

Tout d’abord, gardons en tête que même s’il ne s’agit que d’une indication, le compilateur peut effectivement la suivre. Il a le choix de le faire, mais c’est une possibilité plausible. Il suffit d’imaginer le compilateur le moins intelligent du monde, qui prendrait à la lettre les indications de inline. Il suit parfaitement le standard (selon le standard C++11 §12.1.5) et le mot-clé est utile dans ce cas.

Selon l’article What is C++ inline functions – C++ Articles (cplusplus.com)2, les avantages sont :

  1. Cela accélère le programme en évitant les instructions d’appel de fonction.
  2. Cela économise de l’espace sur la stack car cela ne push/pull pas les paramètres de fonction.
  3. Cela économise l’instruction de retour de la fonction.
  4. Cela créé de la localité de référence, en utilisant le cache d’instruction.
  5. En la marquant comme inline, vous pouvez mettre la définition d’une fonction dans un header (i.e. cela pourra être inclus dans plusieurs unités de compilation sans déranger le linker).

Les points 1, 2 et 3 sont globalement les principaux bénéfices de cette fonctionnalité et le but originel de ce mot-clé. Pour ceux qui connaissent un peu l’assembleur, cela évite notamment de pousser les paramètre de la fonction sur la pile, ce qui coûte de nombreuses instructions.

Le point 4 semble être un effet de bord non négligeable, mais comme le cache d’instruction est loin d’être ma spécialité, je ne m’épancherai pas sur ce sujet.

Le point 5 n’est un avantage que dans certaines situations spécifique, mais un avantage malgré tout.

Désavantages

Selon l’article What is C++ inline functions – C++ Articles (cplusplus.com)2, les désavantages sont :

  1. Cela augmente la taille de l’exécutable.
  2. L’inlining est résolu à la compilation. Cela signifie que si vous changez le code de la fonction inlinée, vous devrez re-compiler tous le code utilisant cette fonction.
  3. Ça augmente la taille de votre header avec des informations qui ne sont pas utiles pour les utilisateurs.
  4. Comme mentionné ci-dessus, cela augmente la taille de l’exécutable, ce qui peut augmenter les fautes de pagination en mémoire, et de ce fait diminuer les performances de votre programme.
  5. Cela peut être dérangeant dans les projets ayant des contraintes de mémoire, comme les projets embarqués.

Les point 1 et 4, qui sont méconnus parmi les développeurs, sont la raison principale pour laquelle le mot-clé inline peut diminuer les performances de votre programme. Il est important de s’en souvenir quand on l’utilise.

Le point 2 peut être un inconvénient majeur, en fonction de la nature de votre projet, mais n’arrive pas excessivement souvent selon mon expérience.

Le point 3 est, d’après moi, le plus gros désavantage de cette fonctionnalité. Pour avoir du code maintenable, vous vous devez d’être clair et organisé. inline est un point noir vis-à-vis de cette notion.

Le point 5 concerne des projets spécifiques, aussi je ne m’étendrai pas dessus. Mais gardez en tête que si vous avez des contraintes de mémoire, inline peut avoir des conséquences.

Conclusion : quand faut-il utiliser inline ?

Éviter les instructions d’appel de fonction en inlinant votre code est seulement utile dans les sections critiques de votre programme.

N’oubliez pas la loi de Paretos : « 80% de l’exécution ne se déroule que dans 20% du code. ». Cela signifie que votre programme passe la plupart de son temps dans des goulots d’étranglement. De fait, si vous inlinez du code qui n’est pas dans un goulot, cela aura peu voire pas d’effet sur vos performances, tout en augmentant la taille de l’exécutable et rendant votre code moins lisible.

Ce qui m’a poussé à écrire cet article est le fait que durant ma vie de codeur, un bon 95% des inline que j’ai pu voir sur des projets industriels étaient utilisé dans du code non-critique.

Il n’y a absolument aucun intérêt à réduire la lisibilité du code dans ce but.

Voici mon conseil : n’utilisez pas inline à moins que vous soyez sûr(e) à 100% qu’il sera appelé dans un goulot d’étranglement.

C’est le seul moyen d’avoir du code à la fois propre et efficace.

Une autre manière d’inliner à éviter

Je vais terminer cet article en disant un mot à propos d’une pratique à particulièrement éviter.

Il s’agit des fonctions implémentées de la manière suivante :

inline void setValue(int i) { m_value = i; }

Cette pratique empêche certains debuggers de faire correctement leur travail.

Par exemple, sous VisualStudio, su vous mettez un point d’arrêt dans cette fonction et qu’il percute, le debugger ne sera pas capable de vous donner la valeur de m_value.

Alors s’il vous plaît, ne faites pas ça. Cela ne coûte pas grand chose de rajouter un ou deux retours à la ligne.

Sur ce, je vous dis à la semaine prochaine !

Article original : Yet another pamphlet about inlining | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

window.h casse la librairie standard

Article original : windows.h breaks the standard library (and my will to live) | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

En travaillant sur du code pas si vieux que ça, j’ai eu une erreur de compilation plutôt singulière.

Voici le code incriminé :

//...
 
const T_LongType BIG_OFFSET = std::numeric_limits<T_LongType>::max() / 2;
 
//...

Et voici les erreurs générées :

1>MyFile.cpp(42): error C2589: '(' : illegal token on the right side of '::'
1>MyFile.cpp(42): error C2059: syntax error : '::'
1>MyFile.cpp(42): error C2059: syntax error : ')'
1>MyFile.cpp(42): error C2059: syntax error : ')'

Elles étaient accompagnée d’une petite volée de warnings que je ne posterai pas ici.

Il m’a fallut pas mal de temps pour comprendre quel était le problème. Après tout, le type T_LongType était correctement défini (un typedef de long) et je n’avais pas oublié d’inclure <limits>.

Peut-être connaissez vous déjà la cause de cela. Il s’agit de la ligne suivant :

#include <windows.h>

En effet, si on regarde à l’intérieur de ce header, on tombe là-dessus :

#ifndef NOMINMAX

#ifndef max
#define max(a,b)            (((a) > (b)) ? (a) : (b))

#ifndef min
#define min(a,b)            (((a) < (b)) ? (a) : (b))

#endif  /* NOMINMAX */

Explication

Le fait que windows.h definisse les macros min et max implique que, pendant la phase de préprocesseur, toutes les instances de min et max sont remplacée par le code de la macro.

Concrètement, cela signifie que lorsque le compilateur compile, au lieu de voir cela :

const T_LongType BIG_OFFSET = std::numeric_limits<T_LongType>::max() / 2;

Il voit cela :

const T_LongType BIG_OFFSET = std::numeric_limits::(((a) > (b)) ? (a) : (b))() / 2;

Ce qui ne fait aucun sens, d’où les erreurs de compilation ci-dessus.

Plusieurs raisons de ne pas inclure windows.h

Voici une liste non-exhaustive expliquant pourquoi inclure ce header est une mauvaise pratique :

  • Il casse la librairie standard juste en l’incluant. On devrait toujours pouvoir utiliser la syntaxe de la librairie standard peut importe les fichiers qu’on inclut.
  • Il vous force à définir NOMINMAX au début de chaque header qui inclut windows.h. Si jamais vous oubliez de le faire, alors tous les fichiers qui incluent le vôtre devront la définir ou risquer de rencontrer les même erreur que moi.
  • C’est un header dependant du system d’exploitation, il faut éviter de l’utiliser tant que c’est possible. Si vous l’utilisez alors que vous pourriez vous en dispenser, alors votre code ne sera plus portable, vous risquez de prendre de mauvaises habitude de codage (en vous reposant trop dessus), sans oublier que plus une librairie est spécifique, moins elle est maintenue.

Conclusion

Il existe des manière d’utiliser windows.h en toute sécurité, mais vous devez être absolument robuste et sans faille, ou vous proveoquerez des effets de bord indésirables.

Tant que vous les pouvez, ne l’utilisez pas.

Article original : windows.h breaks the standard library (and my will to live) | Belay the C++ (belaycpp.com)
Traductrice :
Chloé Lourseyre