[CppCon 2015] Stroustrup : Don’t do that!

La conférence annuelle CppCon 2015 a lieu cette semaine. Il faudra probablement attendre quelques semaines pour avoir les vidéos des présentations sur la chaîne YouTube de CppCon. Cependant, la première vidéo est déjà disponible : le keynote de Bjarne Stroustrup, « Writing Good C++14 » Note de dernière minute : la seconde vidéo « Writing Good C++14… By Default » de Herb Sutter est disponible depuis hier).

On va faire un petit tour de quelques points qui me semblent intéressants, le projet CoreGuideLine sera développé dans un prochain billet.

But do that!

Pour ceux qui suivent régulièrement les discussions sur le forum C++ d’OpenClassRoom à propos du C++ « moderne », beaucoup de choses dans cette présentation ne seront pas des nouveautés. Les problématiques sont bien connues. Par contre, certaines solutions sont originales.

Les erreurs classiques

Les points des plus critiques du C++ « old school » sont les pointeurs et les accès aux tableaux. Pour rappel, pour utiliser un pointeur, celui-ci doit être valide, c’est-à-dire qu’il doit pointer sur un objet valide. Les erreurs proviennent du fait qu’un pointeur est nullptr (qui correspond à une adresse invalide) ou que le pointeur pointe sur un objet invalide (dangling pointer, soit parce que le pointeur n’est pas correctement initialisée, soit parce que l’objet a été détruit).

Object* p = nullptr;
p->f();  // erreur, appel sur nullptr

Object* pp;
pp->f(); // erreur, pointeur invalide

Object* ppp = new Object;
delete ppp;
ppp->f();  // erreur, pointeur invalide

Bien sûr, sur ces codes aussi simples, l’erreur est évidente (bien que beaucoup de personnes ne savent pas en fait qu’un pointeur non initialisé ne vaut pas nullptr, mais prend une valeur aléatoire). Mais on rencontre souvent ces erreurs dans de vrais projets, ce ne sont pas que des problèmes théoriques.

Le second type d’erreur sont les accès en dehors des limites d’un tableau.

int array[5];
array[10] = 0;  // erreur, accès hors limite

Avec les tableaux C++ (std::vector, std::array, etc.), il est possible de tester facilement ce problème en utilisant la fonction membre size et une assertion.

std::vector<int> array(5);
assert(10 < array.size());  // produira un crash ici
array[10] = 0;

Il est possible (et je recommande) de toujours mettre un assert devant un accès à un tableau.

Le problème est plus compliqué avec les tableaux de style C. Ceux-ci ne conservent pas leur taille et il est facile de perdre cette information.

Un dernier type d’erreur est la fuite de mémoire (memory leak). Cela arrive lorsqu’un objet créée dynamiquement n’est pas correctement libéré.

int* p = new int;
p = new int;  // perte du pointeur sur le premier objet créee

int* p = new int[10];
delete p;  // appel de delete au lieu de delete[]

Undefined Behavior

Beaucoup de débutants (ou plus expérimentés) font une erreur classique : les erreurs présentent ne produisent pas d’erreurs de compilation. Et ne produisent pas non plus de crashs. Pas toujours en tout cas. Et jamais en indiquant la ligne de code qui pose problème. Ce type d’erreur produisent ce que l’on appelle un comportement indéfini (Undefined Behavior). Cela signifie que le programme entre dans un état instable et non-prédictible. Il peut continuer à avoir un comportement normal, ne pas crasher, mais donner des résultats faux, ou crasher à n’importe quel moment.

Ce type d’erreur est très difficile à diagnostiquer et parfois à corriger, si la conception de l’application est basée sur une mauvaise utilisation systématiques des pointeurs.

Une solution ?

A cause de ces problèmes et de la difficulté pour les diagnostiquer, le C++ est souvent considéré comme un langage de programmation complexe. La solution proposée par B. Stroustrup est finalement assez simple :

Ne faites pas cela !

Quand on rencontre des problèmes avec une approche, un concept, une syntaxe, le plus simple pour éviter les erreurs est de ne pas faire cela.

Bon, ok, c’est un peu facile. Qu’est-ce que l’on doit faire à la place alors ?

Il existe beaucoup de nouveaux concepts en C++ « moderne », mais ces concepts ne sont pas liés avec une norme du langage en particulier (C++03, C++11 ou plus). Un exemple classique sont les pointeurs « intelligents », beaucoup pensent que c’est un des ajouts majeurs du C++11. Mais ce n’est pas le cas ! La nouveauté du C++11 est simplement de proposer une implémentation de ces pointeurs dans la bibliothèque standard. Les pointeurs intelligents sont utilisables en C++03, en utilisant des bibliothèques (Boost, Qt, etc.) ou en les implémentant soi-même.

Pourtant, nombreux de ces concepts ne sont pas encore assimilés par les développeurs C++ (débutants ou anciens). Les raisons sont multiples, mais il y a un point cité par B. Stroustup qui m’intéressent particulièrement. Dans de nombreux cas, on a (j’ai) tendance, dans les guidelines ou sur les forums comme OpenClassRoom, à donner des règles de façon assez directive (« ne fais pas cela ») sans forcement expliquer le pourquoi et donner l’approche correcte.

(J’exagère un peu, on passe beaucoup de temps à essayer d’expliquer les choses sur les forums, mais on peut probablement faire mieux, plus pédagogique.)

Quelques approches originales

Je ne vais pas entrer dans le détail de toutes les solutions en C++ « moderne » permettant d’éviter ces problèmes (c’est l’objet de mon cours C++), mais détailler deux concepts présentés par B. Stroustrup : owner et not_null. Une proposition d’implémentation par Microsoft est publiée sur GitHub.

Owner

L’implémentation de ce type est tellement simple que je me permets de copier ici le code provenant du GitHub de Microsoft :

template <class T>
using owner = T;

Là, normalement, vous vous dites : « ils se moquent de moi ? Ce type ne fait absolument rien ! »

Et vous auriez parfaitement raison 🙂

Mais analysons la situation plus en détail. Ce type a pour objectif de résoudre le problème de la libération de la mémoire.

Généralement, l’acquisition d’une ressource n’est pas la source principale de problèmes dans un programme. Le plus simple est que chacun est responsable de ses ressources, on les alloue avant de les utiliser et on les libère lorsque l’on n’en a plus besoin. Plus souvent, on va utiliser une ressource qui a été allouée par quelqu’un d’autre. Dans les cas extrêmes, il y aura un responsable dédié pour un type de ressource, tous ceux qui veulent ce type de ressource doivent passer par lui.

Par contre, la libération est plus problématique : personne ne va prendre la responsabilité de libérer la ressource, tout le monde va considérer qu’un autre le fera. Ce problème survient parce que personne n’est clairement désigné pour être le propriétaire d’une ressource, c’est-à-dire celui qui est responsable de la libérer (ownership).

(A mon sens, l’apport principal des pointeurs intelligents du C++11 n’est pas les pointeurs eux-mêmes, mais la réflexion que cela a engendré sur l’importance de définir clairement qui est le propriétaire.)

Le problème est simple : on doit toujours libérer les ressources. Mais comment savoir qui doit le faire et quand le faire ? Prenons un code d’exemple :

void f(int* p) {
    // on reçoit une ressource du code appelant, 
    // doit-on la libérer ?

    int* pp = g();
    // autre ressource reçue, doit-on la libérer ?

    int* ppp = new int;
    h(ppp);
    // on donne une ressource à un autre. Comment
    // dire que l'on souhaite qu'il la libère ?
    // Ou lui dire que l'on va la libérer
    // nous-même ?
}

A chaque fois, il manque un moyen de dire qui est le propriétaire de la ressource, si on devient propriétaire ou pas d’une ressource que l’on reçoit, ou si on donne la propriété ou non d’une ressource à un autre.

C’est donc un simple problème d’expressivité, pourvoir dire ce que l’on souhaite faire. (Qui n’a pas été confronté un jour à un code écrit par un autre et s’être demandé ce qu’il voulait faire ?)

Le rôle de owner est de simplement exprimer cette intention sur la propriété. L’idée est qu’une ressource transmise en utilisant owner transmet la propriété, une ressource transmise par un pointeur nu ne la transmet pas.

void f(int* p);
    // f ne prend pas la propriété, si on lui donne
    // une ressource, il ne va pas la libérer.

void g(owner<int*> p);
    // g prend la responsabilité de la ressource. On
    // n'est plus propriétaire et on ne pas la
    // libérer.

int* h();
    // h transmet une ressource, mais pas la
    // propriété, on ne doit pas la libérer.

owner<int*> i();
    // i transmet une ressource et la propriété,
    // il faut libérer la ressource.

Bien sûr, ce type est purement indicatif pour le développeur, une mauvaise utilisation ne va pas provoquer d’erreur de compilation ou d’exécution. (Il est par contre possible d’utiliser des outils d’analyse statique, qui pourraient prendre en compte ce type d’information).

En suivant une règle simple, on peut alors améliorer la qualité du code, en éviter les problèmes de libération :

Quand on est propriétaire d’une ressource (que ce soit une ressource que l’on a alloué soi-même ou que l’on a reçue), soit on transmet la propriété à un autre (dans une fonction que l’on appelle ou en retour de fonction), soit on libère la ressource.

Il suffit parfois d’améliorer l’expressivité pour améliorer la qualité d’un code.

(Note : certain l’auront compris, owner est une transmission de propriété non partagée. S’il faut avoir plusieurs propriétaires, owner ne pourra pas être utilisée, il faudra utiliser par exemple std::shared_ptr ou trouver un autre moyen d’indiquer la propriété. Je précise quand même que le partage de la propriété devrait être une situation d’exception plus que la règle).

Not_null

La seconde classe n’est pas tellement plus compliquée, elle tient en quelques lignes. Je vous laisse aller voir le code sur le GitHub de Microsoft. L’idée est assez simple : on ne doit pas utiliser un pointeur nullptr ? Il suffit d’interdire à un pointeur de l’être !

Pour cela, la classe not_null, qui possède une sémantique de pointeur, interdit explicitement l’utilisation de nullptr, 0 ou NULL. A la construction :

not_null(std::nullptr_t) = delete;
not_null(int) = delete;

Et pour l’affectation :

not_null<T>& operator=(std::nullptr_t) = delete;
not_null<T>& operator=(int) = delete;

Ces opérations supprimées permettent de produire une erreur lorsque l’on essaie de créer un pointeur nul :

int main() {
    not_null<int*> p = 0;
}

Affiche :

main.cpp:59:20: error: conversion function from 'int
' to 'not_null<int *>' invokes a deleted function
  not_null<int*> p = 0;
                 ^    ~
main.cpp:12:5: note: 'not_null' has been explicitly 
marked deleted here
  not_null(int) = delete;
  ^
1 error generated.

Les opérations arithmétiques sont aussi interdites (cette classe ne doit pas servir pour un pointeur sur un tableau) :

not_null<T>& operator++() = delete;
not_null<T>& operator--() = delete;
not_null<T> operator++(int) = delete;
not_null<T> operator--(int) = delete;
not_null<T>& operator+(size_t) = delete;
not_null<T>& operator+=(size_t) = delete;
not_null<T>& operator-(size_t) = delete;
not_null<T>& operator-=(size_t) = delete;

Il est bien sûr assez facile de « tromper » le compilateur, en faisant une conversion ou en passant par une variable intermédiaire :

int main() {
    not_null<int*> p = static_cast<int*>(nullptr);
}

La sécurité de not_null n’est pas non plus garantie par le compilateur ou lors de l’exécution. C’est surtout une amélioration de l’expressivité du code. Si on manipule un not_null, on sait que l’on ne doit pas passer un pointeur nul ou qui peut être nul.

Basiquement, cela veut dire que l’on ne doit pas affecter un pointeur nu à un not_null :

int* p = f();
not_null<int*> q = p;  // violation du contrat
                       // de not_null

Conclusion

On voit par ces deux exemples simples l’importance de l’expressivité. Une erreur dans un code peut survenir parce que l’on ne sait pas tout de suite ce que l’on doit faire, quand on doit libérer une ressource, quand un pointeur est nul.

L’amélioration du code C++ « moderne » n’est pas simplement une affaire de syntaxe et notions complexes à maîtriser, mais aussi transmettre les bonnes attitudes et questionnements aux développeurs C++, comme se poser la question de la propriété d’une ressource ou la nullité d’un pointeur.

Ou de ne pas utiliser de pointeurs lorsque cela n’est pas nécessaire (utiliser des références, des passages par valeur ou par déplacement).

Publicités

3 commentaires sur « [CppCon 2015] Stroustrup : Don’t do that! »

  1. Salut,

    Très perturbant ce thème, j’ai failli ne pas voir la zone des commentaire à cause de la bande très foncé que j’ai pris pour le pied de page.

    Sinon belle article, comme toujours d’ailleurs.

    C’est en te lissant que je m’aperçois généralement que je ne suis pas informaticien.
    Car bien que je comprenne l’idée, c’est la syntaxe qui ne passe pas :
    template
    using owner = T;
    Un using avec un égal, ça je ne sais pas ce que c’est.

    1. Je rechercherais un autre thème alors 🙂 Le précédent me posait problème parce que le code n’avait une couleur d’arrière plan différente et on le voyait pas très bien. Et j’ai bien de fond un peu sombre, c’est moins fatiguant.

      Pour le using, c’est la nouvelle syntaxe qui remplace typedef en C++11 (le typedef reste valide, mais pour éviter d’utiliser 2 syntaxes qui font la même chose, il vaut mieux l’oublier).

      typedef int MonInt;
      using MonInt = int;

      L’intérêt de using par rapport à typedef, c’est que l’on peut utiliser un template dessus (alias template), comme ici pour owner :

      template<T> using owner = T;

      Cf :
      http://guillaume.belz.free.fr/doku.php?id=nouvelles_fonctionnalites_du_c_11#alias_de_templates
      https://isocpp.org/wiki/faq/cpp11-language-templates#template-alias
      http://en.cppreference.com/w/cpp/language/type_alias

      Et je te rassure : si tu poses la question à plusieurs dev pro C++, il est probable que plusieurs ne connaissent pas cette syntaxe.

  2. J’aimais bien le thème précédent. Je regrette de ne pas avoir fait une petite capture d’écran pour le gardé dans mon stock de graphisme exemple.

    Merci pour ton explication.

    Cette syntaxe de using, j’ai dû la lire quelque part et l’oublié aussi tôt car pas appliqué dans mes codes.

    Donc l’utilisation de owner est possible depuis c++11.
    Et ce n’est qu’une convention purement indicatif pour le développeur comme tu le dis.
    Et on est bien d’accord que si je désassemble le code produit par le compilateur, le code avec owner ou avec pointer est identique.

    Après, il y a toujours la question de la compatibilité de ces pratiques avec les librairies comme Qt par exemple.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s