philippe-dunski

philippe-dunski

Coder efficacement : Bonnes pratiques et erreurs à éviter en C++

A l'origine, il y a mon expérience d'un forum dédié aux professionnels de l'informatique, sur lequel j'ai l'impression de répéter sans cesse les mêmes choses. Au début, j'ai pensé qu'un ticket de blog qui reprendrait une série de considérations sur lesquelles il se passe rarement plus d'un mois avant que je ne doive y revenir suffirait, mais les deux ou trois pages sont devenue cinq puis dix.


J'ai alors envisagé l'écriture d'un article que j'aurais mis à disposition de la communauté du forum en question. C'est bien, un article d'une dizaine de pages, cela se lit en une grosse demi heure. Mais j'avais tellement de choses à dire que les dix pages sont devenues cinquante, puis cent, pour se transformer en un véritable livre.

Le hasard m'a mis en contact avec les éditions D-booker.fr auquel j'ai proposé le manuscrit. Elles ont été emballées. L'ouvrage que vous avez entre les mains est le résultat de notre collaboration.

Vous pourrez vous le procurer aux édition D-booker.fr et dans toutes les bonnes librairies à partir du 18 février 2014.

 

N'hésitez pas à me faire part de vos avis


13/01/2014
2 Poster un commentaire

La vraie responsabilité des références

Bonjour,

 

Je discutais ces derniers jours sur un forum avec quelqu'un qui s'étonnait de trouver une donnée membre de classe prenant la forme d'une référence (constante ou non, cela ne change rien en l'état)

Autrement dit, il s'étonnait de rencontrer une classe qui prenne la forme de

class MaClass{
public:
    MaClass(A /* const */ & a):a_(a){}
private:
    A /*const*/ & a_;
};

Il est vrai que, dans bien des cas, nous croiserons plus volontiers des pointeurs et sans doute même de préférence des pointeurs intelligents, comme les std::shared_ptr, depuis que C+ 11 est sorti.

En effet, le principal problème des pointeurs nus et des références, c'est que l'on n'a absolument aucun moyen de garantir que l'objet pointé ou référencé sera toujours valide lorsque l'on essayera d'y accéder.

 

Pour bien vous faire comprendre le problème, imaginons une classe proche de

 class Test{
public: 
    Test(A * ptr):ptr_{ptr}{}
    void foo(){
        ptr_->doSomething();
    }
private:
    A * ptr_;
};

Cette classe pose énormément de problèmes, ne serait-ce que parce que l'on n'a aucune garantie que ptr_ corresponde bien à une adresse valide.

Il n'y a en effet rien qui interdise à l'utilisateur de cette classe (en dehors de la documentation de la classe elle-même s'entend) d'avoir un code proche de

int main(){
    Test t(nullptr);
    /* ... */
    return 0;
}

En tant que développeur consciencieux de la classe Test, j'aurais donc du me poser la question de savoir ce que représente l'invalidité de ptr_. Car, de deux choses l'une:

  • Soit, il s'agit d'une erreur de la part de l'utilisateur, et j'aurais du veiller à l'en avertir le plus vite possible au travers d'une assertion
  • Soit, il y a "du sens" à permettre à ptr_ de ne pas exister; par exemple, parce qu'il s'agit d'un élément parent et que j'aurais estimé possible l'idée de ne pas avoir de parent.

Dans le deuxième cas, j'aurais normalement du m'assurer de la validité de ptr_ avant de le référencer.

Je ne sais pas moi, au minimum quelque chose qui aurait été proche de

void Test::foo(){
    if(ptr_){
        ptr->doSomething();
    }
}

L'avantage des std::shared_ptr, c'est que la propriété de l'objet pointé est partagée, si bien que cet objet ne sera pas détruit tant que la "dernière copie" du shared_ptr n'aura pas été détruite.

L'inconvénient, c'est que le comptage de référence qui permet à un std::shared_ptr de remplir son devoir, et même la taille d'un std::shared_ptr ne plaident pas en sa faveur, bien qu'il s'agisse souvent d'optimisation prématurée.

Car, hormis le fait de s'assurer que, s'il existait, l'objet pointé continuera d'exister tant que ce sera nécessaire, intelligent ou non, un pointeur se manipulera toujours de la même manière : avant d'être déréférencé, il doit d'avoir été testé au niveau de sa validité.

Et si, pour une raison ou une autre, nous devions garantir le fait que l'objet pointé par ptr_ existe forcément; qu'il ne s'agit plus d'un membre plus ou moins "facultatif", mais bel et bien d'un membre exigé ?

std::shared_ptr nous apporterait la garantie que s'il existe, l'objet pointé ne sera pas détruit.  Mais...  Cela n'apporte aucune garantie que l'objet ait bel et bien été créé avant que le pointeur ne soit déréférencé.  Car, après tout : il peut valoir nullptr parce que l'objet n'a pas encore été créé, mais il peut aussi valoir nullptr parce que l'on aurait trop trainé avec le weak_ptr avant de partager la propriété de l'objet avec une copie du shared_ptr.

Non, décidément, intelligent ou non, un pointeur ne pourra jamais fournir l'ensemble des garanties d'existence de l'objet.  Tout ce qu'il pourra faire, c'est nous permettre de tester si, à un instant T, l'objet est valide ou non.

Et, je ne sais pas pour vous, mais cela m'ennuierait très fort d'avoir à prendre des précautions pour avoir la certitude que quelque chose "qui est sensé ne pas pouvoir ne pas exister" existe réellement.

Mais, quand est-il des références ?

Bahh, les références apportent une garantie de non nullité : l'objet référencé doit impérativement exister lorsque l'on décide de créer une variable (qu'elle soit membre d'une classe ou non) sous la forme d'une référence.  C'est une obligation à laquelle le compilateur n'acceptera aucune entorse.

 D'un autre coté, mon camarade trouvait -- et il semble difficile de le blâmer -- que cela risque très clairement de poser problème si la durée de vie de l'objet "référencé" s'avérait être plus petite que celle de l'objet "référençant".

Observez, pour l'exemple, un peu le code qui vient :

class Referee{
public:
    void doSomething(){
        /* ... */
    }
}; 
class Referer{
public:
   Referer(Referee & r):r_(r){}
   void foo(){
      r_.doSomething();
   }
private:
   Referee & r_;
};
int main(){
   std::vector referees;
   /* on rempli referees */
   std::vector referers;
   /* on rempli referers, en transmettant les références vers le contenu de referees */
   referees.clear();
   for(auto & r : referers){
      r.foo();
   }
   return 0;
}

Vous l'aurez compris, chaque appel r.foo()referee qui n'existe plus, occasionnant forcément un undefined behaviour (le plus souvent, un "plantage").

J'ai donc dit à mon camarade que, si on veut éviter ce problème, il faut imposer par conception (et par construction du code) que les objets référencés aient une durée de vie supérieure à celle des objets référençants, et que, si cette garantie est donnée, cela ne me gène absolument pas d'avoir une donnée membre qui prenne la forme d'une référence. Ainsi, le même code, à peine modifier pour prendre la forme de

class Referee{
public:
    void doSomething(){
        /* ... */
    }
}; 
class Referer{
public:
   Referer(Referee & r):r_(r){}
   void foo(){
      r_.doSomething();
   }
private:
   Referee & r_;
};
int main(){
   std::vector referees;
   /* on rempli referees */
   std::vector referers;
   /* on rempli referers, en transmettant les références vers le contenu de referees */
   for(auto & r : referers){
      r.foo();
   }
   return 0; //referers est détruit en premier
             // puis, c'est referees...
             // et de toutes façons une fois que l'on n'en a plus besoin
}

Mais vous savez que je suis un véritable fanatique des principes SOLID.  Et cette discussion m'a fait m'interroger sur le SRP (Single Responsability Principle ou principe de la responsabilité unique), surtout en ce qui concerne les références et les shared_ptr (qui sont les deux aspects abordés ici).

Mon camarade estimait au départ que c'est à la référence elle-même de garantir le fait que l'objet référencé reste valide.  Mais comment le pourrait-elle?

Une référence, ce n'est jamais qu'un alias, quelque chose destiné à nous facilité la vie grâce à une garantie qui est donnée lors de sa création.

L'un dans l'autre, le SRP aidant, ne peut donc pas avoir d'autre responsabilité que celle-là et, s'il faut effectivement arriver à garantir que la durée de vie référencé sera plus grande que la référence -- quel que soit l'endroit où elle se trouve -- il est forcément nécessaire de le faire "ailleurs".

D'une certaine manière, c'est la principale raison qui fait que l'on dénigre -- à juste titre -- de plus en plus le principe du singleton : si l'on ne veut pas qu'il puisse exister plus d'une instance d'un objet particulier, il faut d'abord et avant tout s'assurer par construction que ce sera le cas : en s'assurant qu'il n'y aura qu'une seule déclaration de variable de ce type, et -- cela devient si simple depuis C++11 -- en en interdisant la copie et l'affectation.

Ceci dit, alors que j'allais valider ce texte pour la parution, j'ai eu une idée assez saugrenue.  Je viens en effet de dire que la référence n'est en mesure de donner des garanties que lors de sa création.

De leur coté, les shared_ptr ne sont en mesure de fournir des garanties qu'à l'utilisation : l'objet pointé ne sera pas détruit tant qu'il sera (susceptible d'être) utilisé.

L'un dans l'autre, ne serait il pas temps de prévoir quelque chose que nous pourrions désigner sous le terme de shared_reference, qui apporterait la garantie de non nullité que peuvent nous apporter les références et la garantie de non destruction de l'objet en cours d'utilisation (ou susceptible d'être utilisé) que peuvent nous apporter les shared_ptr ?  Bien sur, on a les references_wrapper, mais ce n'est pas pareil!

Cette idée est, vous vous en serez doutés, lancée à cause de la fièvre de la rédaction; et je ne suis peut être pas le premier à l'avoir. Et je n'ai aucune idée de la manière dont nous pourrions mettre cela en place.  Mais bon, si quelqu'un a une idée, je suis preneur ;)


10/09/2015
0 Poster un commentaire

Quand microsoft arrêtera-t-il?

Quand microsoft arrêtera-t-il de se foutre de la gueule du monde?Décidément, plus cela va, plus mon ressentiment augmente vis à vis de la mentalité de microsoft.  Je suis un professionnel du développement.  Très spécialisé, je ne le nie absolument pas, mais un professionnel averti.  En tant que tel, il est normal que je fasse ce que l'on appelle la "veille technologique".

L'idée est que je suis suffisamment passionné par mon travail et suffisamment spécialisé pour décider de me tenir le plus possible des évolutions qui surviennent dans mon domaine.

Cela va du fait de me tenir au courant des évolutions de mon langage de prédilection qu'est le C++ au fait de garder un oeil sur la disponibilité des différentes possibilités du langage -- surtout les plus récentes -- au niveau des différents outils que le développeur est susceptible d'utiliser.

Il serait en effet particulièrement difficile d'expliquer correctement quelque chose au gens que je ne connais pas moi-même : Si je veux avoir un minimum de crédibilité et conseiller au mieux les gens qui s'adressent à moi, il faut que je sache de quoi je parle.  C'est vrai aussi bien quand il s'agit d'expliquer une fonctionnalité (récente ou plus ancienne) que lorsqu'il s'agit de revenir sur des principes de conception, mais aussi lorsqu'il s'agit d'aider les gens à utiliser leur environnement de développement particulier.

A titre privé, je fonctionne avec un ordinateur que j'ai monté de toutes pièces il y a quelques années déjà.  Bien que n'étant pas forcément fan de windows, j'ai pris la peine d'acheter une version officielle de vista, car c'était la version de windows la plus récente à l'époque, qui fonctionne parfaitement à sur une machine virtuelle installée sur mon linux.Sous vista, il est parfaitement possible d'installer les versions 2005, 2008 et 2010 de Visual studio, tant dans sa version express que dans sa version professionnelle. J'ai personnellement eu l'occasion de travailler dans des équipes qui utilisaient ces différentes version et les connais donc suffisamment pour conseiller et orienter les gens qui seraient confrontés à des problèmes avec eux.  Pour les éventuels aspects qu'il me faudrait revoir, j'ai simplement installé la version express sur ma machine virtuel, histoire de rester dans la légalité, mais aussi parce que, a priori, les débutants se tourneront vers cette version "allégée" de l'EDI.Le problème, c'est que ces versions sont malheureusement antérieures à la dernière norme C++ en date, connue sous le doux nom de C++11. Et comme on tend de plus en plus à inciter les gens à en utiliser les fonctionnalités nouvelles, pour la simple et bonne raison que ces dernières apportent énormément de facilité et une sécurité de développement accrue, il est tout à fait normal que l'on essaye d'inciter les gens à ... utiliser des versions récentes de leur compilateur préféré.

Avant la sortie de Visual Studio 2012, microsoft l'avait promis, juré craché: Visual studio 2012 respectera l'intégralité de la norme.  Le résultat fut décevant. Car le support de la norme s'avéra pour le moins incomplet pour la toute première version.

Pensez donc: la liste des fonctionnalités indisponibles est si grande que j'ai décider de ne pas la présenter ici.  Mais vous pourrez vous en faire une idée sur la page même de microsoft.

Soit! Il est vrai que C++ est venu avec de nombreuses nouveautés et que, même si la norme a mis longtemps à être finalisée, on peut éventuellement comprendre que leur développement prenne du temps.  A ceci près que le support de C++11 était, à la même époque, bien plus avancé sur Gcc, et plus encore sur clang.

Avec un peu de retard, visual studio express 2013 est disponible depuis près d'un an maintenant, j'ai décidé de m'intéresser à la version 2013. Et les motifs de déception n'ont pas manqué!

D'abord, sachez que vous aurez au minimum besoin de windows 7 pour pouvoir l'installer.  Alors, j'admets volontiers que windows vista n'a pas duré très longtemps et que microsoft a sans doute (selon l'avis de certains en tout cas) très bien fait de le remplacer assez vite.  Bien que je n'ai personnellement jamais eu le moindre problème avec, y compris avant de faire la transition d'une machine virtuelle linux tournant sous windows à une machine virtuelle windows tournant sous linux.

Et, si vous êtes intéressés par certaines fonctionnalités propres à visual studio, comme la possibilité de proposer vos développement sur l'app store de microsoft, ne sont disponible que pour les pc tournant sous windows 8.1!  Ce n'est qu'un détail pour moi, car je préfères fournir mes développements perso sous licence open source, mais soyez donc prévenus : si vous souhaitez pouvoir placer le résultat de vos développements sur l'app-store de microsoft, vous serez face à l'obligation d'utiliser windows 8.1.

Mais le pire de tout, c'est que de nombreuses fonctionnalités de C++11 ne sont toujours pas implémentées dans visual studio 2013! (référez vous à la page citée un peu plus haut pour en savoir d'avantage).

Voici donc une société qui abuse déjà fortement d'une position dominante, et qui, en plus :

  1. se permet de proposer trois versions de son environnement de développement phare sur trois ans;
  2. se permet de faire payer "cher et vilain" la licence d'utilisation de ces environnement lorsqu'ils sont destinés à un usage commercial ;
  3. se permet de forcer les éventuels utilisateurs à changer de système d'exploitation s'ils veulent profiter pleinement des possibilité de la dernière version ;
  4. qui semble décidément incapable de fournir un support correct d'une norme déjà clairement établie et correctement suivie par ses concurrents.

Mais de qui se moque donc microsoft? Ne voit-elle vraiment les utilisateurs que comme des vaches à lait? Quand décidera-t-elle enfin d'avoir un peu plus de considération pour ses clients?

Je suis le premier à saluer les qualités de ces environnements de développement, bien que je les trouve personnellement particulièrement lourds.  Mais, très sincèrement, quand on sait qu'il est particulièrement facile de disposer d'autres environnements tout aussi efficaces gratuits, plus légers et surtout associés à des compilateur qui respectent la norme, comment s'étonner que l'on préfère régulièrement conseiller les concurrents?


09/02/2014
5 Poster un commentaire

Java: sale menteur!

L'autre jour, je discutais avec l'un de mes javaistes d'amis.  Après tout, il y a des gens très bien qui développent en java!

Bien sûr, comme je suis plutôt orienté vers le C++, la discussion n'a pas traîné à dériver sur les avantages comparés de java et de C++.  C'est le coup classique!

Lorsque j'en suis arrivé à discuter de l'héritage multiple, il s'est écrié: "Héritage multiple, bons dieux, quelle horreur!". 

J'ai alors pris un exemple classique : Comment créer une classe TurboGenerateur lorsqu'on dispose d'une classe Turbine et d'une classe Generateur ? Après tout, LSP est parfaitement respecté si l'on décide de faire hériter notre classe TurboGenerateur de Turbine et de Generateur, vu qu'un tel objet est, bel et bien, tout à la fois une turbine et un générateur et que l'on retrouve bel et bien, au niveau de TurboGenerateur, tout ce que l'on trouve aussi bien dans l'une que dans l'autre.

- "Et les interfaces, c'est fait pour les cochons? m'a-t-il répondu."

- "Bien sur que non, mais si on a deux développeur -- l'un qui décide de mettre l'accent sur l'aspect Turbine, implémentant également l'interface IGenerateur et l'autre qui décide de mettre l'accent sur l'aspect Generateur, implémentant également  l'interface ITurbine-- on fait quoi?" lui demandai-je. "On se retrouve avec deux classes totalement différentes, qui font exactement la même chose, mais qu'on ne peut pas substituer l'une à l'autre?"

Il m'a alors expliqué que, si Turbine implémentait l'interface ITurbine et que Generateur implémentait l'interface IGenerateur, il n'y aurait aucun problème : la substitution pourrait parfaitement se faire aussi bien en transmettant une ITurbine qu'un IGenerateur.  Il a, sur ce point, totalement raison!

Il faut alors se rendre compte que, lorsque vous décidez en java qu'une de vos classe implémente une interface, vous ne faites rien d'autre que de créer une relation d'héritage au termes du LSP.  Bien sur, votre interface ne fait qu'exposer des comportements "à définir dans la classe qui les implémente", mais il n'y a aucune différence -- hormis le mot clé utilisé pour la définir (interface au lieu de class) -- entre une interface et une classe.

Enfin, ce n'est pas tout à fait vrai, dans le sens où vous devrez utiliser le mot clé inherits pour faire hériter votre classe particulière d'une classe donnée et le mot clé implements pour faire en sorte que votre classe particulière implémente une interface donnée.

Mais un fait demeure : il n'y a, conceptuellement parlant, aucune différence aux termes de LSP entre la relation qui unit une classe dérivée à la classe de base déclarée à l'aide du mot clé inherits et la relation qui unit une classe dérivée et l'interface implémentée à l'aide du mot clé implements.

La seule différence qui existe n'est jamais qu'une différence "artificielle" imposée par java au travers des différents mots clé.

La conclusion logique, c'est que java vous ment lorsqu'il vous impose d'éviter l'héritage multiple! Chaque fois que vous décidez de faire hériter une classe d'une autre et de lui faire implémenter une interface, ou chaque fois que vous décidez qu'une classe implémente plusieurs interface, vous ne faites absolument rien d'autre que créer des relations d'héritage multiple!

Amis javaistes, pensez à cela avant de vous lancer dans une diatribe contre l'héritage multiple : vous en êtes très certainement les plus gros utilisateurs.  Votre langage préféré l'a élevé au rang d'institution en créant la notion d'interface. La seule chose, c'est que vous n'en avez peut être pas conscience à cause des mots clés que vous utilisez.


16/01/2014
7 Poster un commentaire

C++, multi-paradigme? non, multi optiques, au mieux

Bjarne Stroutrup a bien raison de ne pas aimer entendre parler de "langage muti-paradigme" lorsqu'on parle de C++.  En effet, selon wikipedia, un paradigme est une représentation du monde, une manière de voir les choses, un modèle choérent de vision du monde <...> une forme de rail de la pensée dont les lois ne doivent pas être confondue avec celles d'un autre paradigme.

Autrement dit, si l'on en croit wikipedia, à partir du moment où l'on a décidé d'un paradigme pour le développement d'un projet, on devient véritablement l'esclave de cette décision, sans retour en arrière et sans compromis possible.

Cela se remarque entre autres pour des langages comme java ou comme C# qui obligent à créer une fonction membre, statique, nommée main pour au minimum une classe et ce, même si cette fonction devait être la seule fonction de cette classe. Ces langages appliquent à la lettre la définition d'un paradigme telle qu'elle est donnée par wikipedia et considèrent que "tout est objet".  Même leur generics (ou ce qui en tient lieu) sont, d'abord et avant tout apparentés des objets, bien que le développeur puisse ne pas forcément en avoir conscience.

Le gros malheur, c'est que cette approche "formate" le schéma de pensée de nombreux développeurs, y compris celui de développeurs qui ne sont pas soumis aux restrictions d'un paradigme unique : Combien de fois n'assistons nous pas à une discussion dans laquelle un intervenant vient nous asséner un "Mais pourquoi ne fais tu pas une classe?" ou un "Mais tu dois penser en termes d'objets" ?

En C++, de telles affirmations perdent beaucoup de leur sens.  N'allez pas me faire dire ce que je n'ai pas dit : il arrive très régulièrement qu'elles soient parfaitement sensées dans une situation particulière bien spéciale.  Mais il arrive aussi très régulièrement qu'un des "autres paradigmes" proposé par C++ fasse aussi bien l'affaire, sinon mieux.

Je pense bien sûr au paradigme générique, mais on peut aussi compter sur le paradigme purement procédural.

Je conseillerais donc au lecteur de ce ticket d'accepter l'éventualité que le paradigme orienté objet -- qui apporte il est vrai beaucoup de réponses -- puisse ne pas être adapté à toutes les situations, et d'accepter d'élargir ses horizons afin d'accepter d'envisager le paradigme procédural et le paradigme générique de la même manière que l'orienté objet.  Vous aurez tout à y gagner, car vous pourrez alors choisir "le meilleur" de chaque paradigme, et vos développements seront plus faciles à maintenir, à corriger et à faire évoluer.


14/01/2014
1 Poster un commentaire