Le C++ expressif n° 3 : pourquoi les erreurs des templates posent des problèmes et qu'est-ce que vous pouvez faire pour ça ?

Eric Niebler

Bienvenue dans le troisième article de la série « le C++ expressif », une série d'articles consacrés aux langages orientés domaine enfoui (EDSL : Embeded Domain-Specific Languages) et à Boost.Proto, une bibliothèque pour les implémenter en C++. Le titre de cet article est volontairement provocateur, pour me donner la liberté créative dont j'ai besoin pour libérer cette juste colère hors de moi, pour indiquer les parts de responsabilités largement méritées, et après cette purification, proposer quelques suggestions constructives pour améliorer la situation. Vous pourriez être surpris de là où je dirige ma colère et heureux aussi de savoir que si vous êtes un auteur ou un utilisateur de bibliothèque, il y a des choses que vous pouvez faire pour aider à améliorer la situation.

Retrouvez l'ensemble des articles de la série « Le C++ expressif » sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Le C++ expressif n° 3 : pourquoi les erreurs des templates posent des problèmes et qu'est-ce que vous pouvez faire pour ça ?

- Auteur : Eric Niebler

- Parution : 23 août 2012

- Licence :

Sommaire

- I. Introduction

- II. Les erreurs de templates : un triste état des choses

- III. Assainissement de logiciel pour les nuls

- IV. Les bibliothèques de formatage Mad1 revisitées

- V. Les grammaires dans Proto

- VI. Valider des expressions suivant des grammaires

- VII. Erreurs à éviter

- VIII. Conclusions et ce qui va venir ensuite

- IX. Remerciements

I - Introduction

Bienvenue dans le troisième article de la série « le C++ expressif », une série d'articles consacrés aux langages orientés domaine enfoui (EDSL(1): Embeded Domain-Specific Languages) et à Boost.Proto, une bibliothèque pour les implémenter en C++. Le titre de cet article est volontairement provocateur, pour me donner la liberté créative dont j'ai besoin pour libérer cette juste colère hors de moi, pour indiquer les parts de responsabilités largement méritées, et après cette purification, proposer quelques suggestions constructives pour améliorer la situation. Vous pourriez être surpris de là où je dirige ma colère et heureux aussi de savoir que si vous êtes un auteur ou un utilisateur de bibliothèque, il y a des choses que vous pouvez faire pour aider à améliorer la situation.

Ensuite, nous allons revenir aux EDSL et appliquer mes recommandations à l'exemple du formatage d'une chaîne de caractères que nous avons développé dans l'article précédent. À la fin de l'article, vous saurez comment vérifier une syntaxe avec un EDSL en définissant sa grammaire, à valider qu'une expression correspond à cette grammaire et à établir un diagnostic court et pertinent si ce n'est pas le cas.

II - Les erreurs de templates : un triste état des choses

Dernières nouvelles dans le monde du C++(2) :

LES MESSAGES D'ERREURS DES TEMPLATES SONT TERRIBLES !

‹bâillement> Ce n'est pas une nouvelle pour celui qui a utilisé le C++ ces dix dernières années. Même de simples mauvaises utilisations de bibliothèques de templates peuvent conduire à des messages de 100 Ko crachés par le compilateur. Qui est à blâmer ? Faites votre choix : les auteurs de la bibliothèque, les fournisseurs de compilateur ou la commission de normalisation du C++ ? Ils ont chacun leur part de responsabilité. Le principal argument de vente des concepts du C++0x (RIP) était d'améliorer les erreurs de template. Et l'un des principaux arguments de vente de Clang, un nouveau compilateur C/C++ passionnant en développement actif, est de produire de meilleurs messages d'erreur. Mais selon mon expérience personnelle en tant que développeur de bibliothèques, je crois que ce problème commence à la base : avec des bibliothèques de templates mal conçues et mal implémentées.

Les techniques pour améliorer la détection des erreurs de compilation et leurs descriptions dans les bibliothèques existent depuis un certain temps, mais elles ne sont pas habituellement connues ou largement utilisées(3). Si les gens comprenaient que le monde serait beaucoup mieux si ces techniques avaient été appliquées de façon uniforme, on ne se contenterait pas de messages de 100 ko crachés par les compilateurs. Nous serions scandalisés.

Si je ne suis pas assez clair, permettez-moi de le dire explicitement et d'une manière qui risque de soulever quelques sourcils :

De mauvaises erreurs de templates sont des bogues de la bibliothèque et devraient être signalés comme tels.

C'est donc mon grand conseil pour les utilisateurs de bibliothèques ? Se plaindre ? Oui, mais se plaindre aux bonnes personnes. Pas sur votre blog, pas dans Reddit, mais à l'auteur de la bibliothèque incriminée. Et le faire en déposant un bug reproductible. Si tout le monde fait ça, les auteurs de la bibliothèque recevront le message que ces problèmes peuvent et doivent être fixés.

L'implication des utilisateurs de bibliothèques est simple : arrêtez de maudire dans le vide et commencez à maudire les auteurs de bibliothèques. En fait, ne les maudissez pas, ça pourrait être moi. Soumettez des bogues à la place. Oui, vraiment. (Et si vous ne pouvez pas attendre que les bogues soient corrigés, passez à Clang ou installez STLFilt.)

Quelles sont les implications pour les auteurs de bibliothèques ? Que pourrait faire un auteur de bibliothèques pour corriger ces soi-disant « bogues » ? Et avant tout, pourquoi y a-t-il une endémie d'erreurs de mauvais template ? Disons-le simplement : un manque total de validation des paramètres.

III - Assainissement de logiciel pour les nuls

Lorsque vous commencez l'écriture d'un code, quelqu'un vous a probablement dit combien il est important de valider les paramètres (à l'exécution) pour les limites de l'API. Les pointeurs NULL, les indices hors limite, les URL sans échappement - si vous négligez de les vérifier, vous allez vous retrouver avec des bogues d'exécution que des pirates pourraient exploiter. N'importe quel programmeur digne de ce nom vous dira cela.

Presque tous les templates nécessitent certaines conditions pour leurs paramètres : qu'ils aient certaines fonctions membres, certains typedefs imbriqués, etc. Valider un paramètre template revient à vérifier que ces conditions sont remplies (dans la mesure du possible) et ce n'est pas un véritable diagnostic si cette vérification n'est pas faite.

Mais quand ces mêmes programmeurs se posent pour écrire un template, beaucoup ont tendance à oublier ce conseil très basique et acceptent allègrement les types fournis par les utilisateurs sans faire aucune vérification des paramètres. Le résultat est une épave aux proportions épiques.

Prenons l'exemple donné dans l'introduction de Boost.Spirit et modifions-le légèrement :

1. #include <boost/spirit/home/qi.hpp>

3. int main()

4. {

5. using namespace boost::spirit::qi;

6. rule<char const *> expression, term, factor;

8. expression  = term >> *( ( '+' >> term ) | ( '-' >> term ) ) ;

9. term        = factor >> *( ( '*' >> ~factor ) | ( '/' >> factor ) ) ;

10. factor      = uint_ | '(' >> expression >> ')' | '-' >> factor ;

11. }

Pouvez-vous repérer la faute de frappe dans le code ?

Réponse

C'est le tilde avant 'factor' sur la ligne 9

Le message de 160 ko craché par le compilateur est suffisant pour faire hurler n'importe quel développeur sain d'esprit(4) :

In file included from /home/Eric/boost/org/trunk/boost/spir it/home/qi/char.hpp:14:0, from /home/Eric/boost/org/trunk/boost/spir it/home/qi.hpp:17, from main.cpp:1: /home/Eric/boost/org/trunk/boost/spirit/home/qi/char/char_p arser.hpp: In instantiation of ?boost::spirit::qi::make_com posite<boost::proto::tag::complement, boost::fusion::cons<b oost::spirit::qi::reference<const boost::spirit::qi::rule<c onst char*> >, boost::fusion::nil>, boost::fusion::unused_t ype, void>?: /home/Eric/boost/org/trunk/boost/spirit/home/qi/meta_compil er.hpp:103:13: instantiated from ...

<snip enormous template instantiation backtrace> (Ndlr, Énorme pile d'instanciations de templates)

/home/Eric/boost/org/trunk/boost/mpl/if.hpp:70:41: error: ? value? is not a member of ?boost::spirit::traits::has_no_un used<boost::fusion::transform_view<boost::fusion::cons<boos t::spirit::qi::sequence<boost::fusion::cons<boost::spirit:: qi::literal_char<boost::spirit::char_encoding::standard, tr ue, false>, boost::fusion::cons<boost::spirit::qi::negated_ char_parser<boost::spirit::qi::reference<const boost::spiri t::qi::rule<const char*> > >, boost::fusion::nil> > >, boos t::fusion::cons<boost::spirit::qi::sequence<boost::fusion:: cons<boost::spirit::qi::literal_char<boost::spirit::char_en coding::standard, true, false>, boost::fusion::cons<boost:: spirit::qi::reference<const boost::spirit::qi::rule<const c har*> >, boost::fusion::nil> > >, boost::fusion::nil> >, bo ost::spirit::traits::build_attribute_sequence<boost::fusion ::cons<boost::spirit::qi::sequence<boost::fusion::cons<boos t::spirit::qi::literal_char<boost::spirit::char_encoding::s tandard, true, false>, boost::fusion::cons<boost::spirit::q i::negated_char_parser<boost::spirit::qi::reference<const b oost::spirit::qi::rule<const char*> > >, boost::fusion::nil > > >, boost::fusion::cons<boost::spirit::qi::sequence<boos t::fusion::cons<boost::spirit::qi::literal_char<boost::spir it::char_encoding::standard, true, false>, boost::fusion::c ons<boost::spirit::qi::reference<const boost::spirit::qi::r ule<const char*> >, boost::fusion::nil> > >, boost::fusion: :nil> >, boost::spirit::context<boost::fusion::cons<boost:: fusion::unused_type&, boost::fusion::nil>, boost::fusion::v ector0<> >, boost::mpl::identity, const char*>::element_att ribute, boost::fusion::void_> >?

L'erreur est que la définition de la règle term n'est pas valide, mais il n'est pas facile de le déduire de cette montagne de déchet.

Analyse lors de la compilation ou lors de l'exécution

Pourquoi de bons développeurs ignorent-ils les bonnes pratiques de programmation quand ils écrivent des templates(5) ? Selon mon expérience personnelle, je peux dire qu'il m'a fallu un certain temps avant d'apprécier les ressemblances entre l'instanciation d'un template (analyse lors de la compilation) et l'appel d'une fonction (analyse lors de l'exécution), et plus de temps encore pour que je réalise que les principes d'ingénierie qui s'appliquent à l'exécution - comme paramètre de validation - peuvent aussi être appliqués lors de la compilation. Maintenant que je le sais, ça semble être une évidence aveuglante.

Le problème de la détection des erreurs et de leur description est particulièrement grave pour les EDSL. Un langage orienté domaine aura généralement des erreurs spécifiques à ce domaine dont le compilateur C++ ignore tout. Toutes les erreurs que le compilateur est autorisé à émettre seront probablement de trop bas niveau pour avoir un sens pour l'utilisateur de l'EDSL (comme cette horrible erreur de Spirit). La bibliothèque a besoin de détecter et signaler les erreurs spécifiques au domaine. Heureusement, si vous utilisez Boost.Proto, vous avez des outils puissants à votre disposition(6). Voyons en détail comment appliquer mon conseil sur l'EDSL que j'ai développé dans l'article précédent.

IV - Les bibliothèques de formatage Mad1 revisitées

L'API de formatage des chaînes proche de la bibliothèque Mad(7) réalisée dans le dernier article permet de formater les chaînes et de spécifier les relations en ligne en utilisant une syntaxe proche similaire à une map. Une utilisation typique ressemble à ceci :

1. std::cout << format("The home directory of {user} is {home}\n"

2. , map("user", "eric")

3. ("home", "/home/eric") );

Cette expression devrait afficher :

The home directory of eric is /home/eric

La partie EDSL est le second argument de la fonction format. Puisque nous allons devoir y faire de nombreuses références, je vais reproduire l'exemple complet du dernier article :

1. #include <map>

2. #include <string>

3. #include <iostream>

4. #include <boost/proto/proto.hpp>

5. #include <boost/xpressive/xpressive.hpp>

6. #include <boost/xpressive/regex_actions.hpp>

8. struct map_ {};

9. boost::proto::terminal<map_>::type map = {};

11. typedef std::map<std::string, std::string> string_map;

13. // Fonction récursive pour remplir la map

14. template< class Expr >

15. void fill_map( Expr const & expr, string_map & subs )

16. {

17. using boost::proto::value;      // lire une valeur depuis un nœud terminal

18. using boost::proto::child_c;    // récupère le n-ième enfant d'un nœud non terminal

19. subs[ value(child_c<1>( expr )) ] = value(child_c<2>(expr));

20. fill_map( child_c<0>(expr), subs );

21. }

23. // Le 'map' terminal termine la récursion

24. void fill_map( boost::proto::terminal<map_>::type const &, string_map & )

25. {}

27. // L'ancienne interface de format, qui accepte une map de chaînes à substituer

28. std::string format( std::string fmt, string_map & subs )

29. {

30. namespace xp = boost::xpressive;

31. using namespace xp;

32. sregex const rx = '{' >> (s1= +_w) >> '}';        // identique à "{(\\w+)}"

33. return regex_replace(fmt, rx, xp::ref(subs)[s1]);

34. }

36. // La nouvelle interface qui complète l'ancienne

37. template< class Expr >

38. std::string format( std::string fmt, Expr const & expr )

39. {

40. string_map subs;

41. fill_map( expr, subs );

42. return format( fmt, subs );

43. }

45. int main()

46. {

47. std::cout << format("The home directory of {user} is {home}\n"

48. , map("user", "eric")

49. ("home", "/home/eric") );

50. }

Fill_map s'attend à ce qu'on lui donne des arbres d'expression d'une certaine forme. Mais remarquez comment la seconde surcharge de format prend une map comme paramètre et la transfère tout simplement à fill_map à la ligne 41, sans aucune validation de paramètres. Mettons la pagaille dans l'arbre d'expression et voyons ce qui se passe :

1. std::cout << format("The home directory of {user} is {home}\n"

2. , map("user", L"eric")

3. ("home", "/home/eric") );

Notez que j'ai changé une chaîne littérale étroite en une chaîne littérale large. Quand je recompile le code avec ce changement, j'obtiens un message d'erreur de plus de 50 lignes.

Cliquez ici pour voir le message d'erreur(8) :

Voir le message d'erreur

1>------ Build started: Project: scratch, Configuration: Debug Win32 ------
1>  scratch.cpp
1>c:\scratch.cpp(60): error C2679: binary '=' : no operator found which takes a right-hand operand of type 'const wchar_t [5]' (or there is no acceptable conversion)
1>          c:\program files\microsoft visual studio 10.0\vc\include\xstring(707): could be 'std::basic_string<_Elem,_Traits,_Ax> &std::basic_string<_Elem,_Traits,_Ax>::operator =(std::basic_string<_Elem,_Traits,_Ax> &&)'
1>          with
1>          [
1>              _Elem=char,
1>              _Traits=std::char_traits<char>,
1>              _Ax=std::allocator<char>
1>          ]
1>          c:\program files\microsoft visual studio 10.0\vc\include\xstring(762): or       'std::basic_string<_Elem,_Traits,_Ax> &std::basic_string<_Elem,_Traits,_Ax>::operator =(const std::basic_string<_Elem,_Traits,_Ax> &)'
1>          with
1>          [
1>              _Elem=char,
1>              _Traits=std::char_traits<char>,
1>              _Ax=std::allocator<char>
1>          ]
1>          c:\program files\microsoft visual studio 10.0\vc\include\xstring(767): or       'std::basic_string<_Elem,_Traits,_Ax> &std::basic_string<_Elem,_Traits,_Ax>::operator =(const _Elem *)'
1>          with
1>          [
1>              _Elem=char,
1>              _Traits=std::char_traits<char>,
1>              _Ax=std::allocator<char>
1>          ]
1>          c:\program files\microsoft visual studio 10.0\vc\include\xstring(772): or       'std::basic_string<_Elem,_Traits,_Ax> &std::basic_string<_Elem,_Traits,_Ax>::operator =(_Elem)'
1>          with
1>          [
1>              _Elem=char,
1>              _Traits=std::char_traits<char>,
1>              _Ax=std::allocator<char>
1>          ]
1>          while trying to match the argument list '(std::basic_string<_Elem,_Traits,_Ax>, const wchar_t [5])'
1>          with
1>          [
1>              _Elem=char,
1>              _Traits=std::char_traits<char>,
1>              _Ax=std::allocator<char>
1>          ]
1>          c:\scratch.cpp(61) : see reference to function template instantiation 'void fill_map<Expr>(const Expr &,string_map &)' being compiled
1>          with
1>          [
1>              Expr=const boost::proto::exprns_::expr<boost::proto::tag::function,boost::proto::argsns_::list3<boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<map_>,0> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const wchar_t (&)[5]>,0>>,3>
1>          ]
1>          c:\scratch.cpp(76) : see reference to function template instantiation 'void fill_map<Expr>(const Expr &,string_map &)' being compiled
1>          with
1>          [
1>              Expr=boost::proto::exprns_::expr<boost::proto::tag::function,boost::proto::argsns_::list3<const boost::proto::exprns_::expr<boost::proto::tag::function,boost::proto::argsns_::list3<boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<map_>,0> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const wchar_t (&)[5]>,0>>,3> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[11]>,0>>,3>
1>          ]
1>          c:\scratch.cpp(84) : see reference to function template instantiation 'std::string format<boost::proto::exprns_::expr<Tag,Args,Arity>>(std::string,const Expr &)' being compiled
1>          with
1>          [
1>              Tag=boost::proto::tag::function,
1>              Args=boost::proto::argsns_::list3<const boost::proto::exprns_::expr<boost::proto::tag::function,boost::proto::argsns_::list3<boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<map_>,0> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const wchar_t (&)[5]>,0>>,3> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[11]>,0>>,
1>              Arity=3,
1>              Expr=boost::proto::exprns_::expr<boost::proto::tag::function,boost::proto::argsns_::list3<const boost::proto::exprns_::expr<boost::proto::tag::function,boost::proto::argsns_::list3<boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<map_>,0> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const wchar_t (&)[5]>,0>>,3> &,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[5]>,0>,boost::proto::exprns_::expr<boost::proto::tag::terminal,boost::proto::argsns_::term<const char (&)[11]>,0>>,3>
1>          ]
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

L'erreur se produit au plus profond de l'implémentation de notre EDSL. Si nous avions validé le paramètre expr avant d'appeler fill_map, nous aurions pu faire beaucoup mieux. Voyons comment.

V - Les grammaires dans Proto

À première vue, la validation du paramètre expr s'annonce difficile. Après tout, l'utilisateur peut passer l'une des nombreuses expressions de map de profondeur arbitraire. Mais quand nous regardons avec le point de vue conception de langage, ce problème semble beaucoup plus simple : nous avons juste besoin de trouver la grammaire à laquelle toutes les expressions de la carte doivent se conformer. Alors, nous avons juste à vérifier que l'expression correspond à la grammaire.

Cette expression :

1. map("user", "eric")

2. ("home", "/home/eric")

… construit un arbre d'expression dans Proto qui ressemble à ceci :

Figure 1 : arbre d'expression correspondant à une map

En clair, nous pouvons décrire la structure des arbres d'expression d'une map de la façon suivante :

- l'expression d'une map est soit :

- le nœud terminal d'une map ou

- l'appel d'une fonction ternaire, dans laquelle :

- le premier enfant est un arbre valide d'expression d'une map (notez la récursion),

- le second enfant est une chaîne et

- le troisième enfant est également une chaîne.

En utilisant le support de Proto pour définir des grammaires, nous pouvons définir MapGrammar comme indiqué dans le code suivant (les explications sont en dessous) :

1. // Définit la grammaire d'une expression de map

2. struct MapGrammar

3. : proto::or_<

4. // Une expression de map est un nœud terminal ou

5. proto::terminal<map_>

6. // ... une fonction ternaire non terminale dont les nœuds enfants sont

7. , proto::function<

8. // ... une expression de map,

9. MapGrammar

10. // ... un nœud terminal limité à une chaîne et

11. , proto::terminal<char const *>

12. // ... un autre nœud terminal limité à une chaîne.

13. , proto::terminal<char const *>

14. >

15. >

16. {};

Reprenons ce code point par point.

- Ligne 2 : les grammaires dans Proto sont de simples structures définies par l'utilisateur.

- Ligne 3 : l'héritage est utilisé pour dire que MapGrammar est exprimé en termes de proto::or_. proto::or_ est utilisé pour les grammaires alternatives, comme l'opérateur | dans l'EBNF : « une expression autorisée peut correspondre à ceci ou cela ». Dans Proto, les grammaires alternatives sont testées dans l'ordre.

- Ligne 5 : une expression dans une map peut être un simple nœud terminal de cette map. Notez que proto::terminal‹map_> a été utilisé pour définir la variable globale map à la ligne 9 de l'exemple complet. Il est également utilisé ici comme une grammaire qui correspond à ce nœud terminal.

- Ligne 7 : proto::function définit une grammaire qui correspond à un nœud d'une expression dans Proto, créé par les opérateurs surchargés d'appel de fonction operator(). Proto fournit des modèles comme function pour tous les opérateurs surchargés par Proto. Une liste complète peut être trouvée dans la documentation de Proto.

- Ligne 9 : le premier enfant du nœud function doit correspondre à MapGrammar. C'est intéressant ! Il semble que nous définissons récursivement MapGrammar par rapport à lui-même. Ce qui est légal, étonnamment. En fait, vous êtes peut-être déjà familier avec cette technique. C'est ce qu'on appelle un motif de template curieusement récurrent (CRTP : Curiously Recurring Template Pattern). Il permet à Proto de définir de façon naturelle les grammaires récursives.

- Lignes 11-16 : rien de très surprenant. Les deux autres enfants doivent se limiter à des chaînes terminales. La structure MapGrammar en elle-même est vide. C'est toujours le cas pour les grammaires Proto.

Faisons le point

À l'heure actuelle, vous pourriez vous sentir un peu dépassés. Nous venons de couvrir beaucoup de nouveaux sujets et ce style de codage pourrait vous sembler étrange. Mais examinons un instant ce que nous venons d'expliquer et comment l'exprimer de façon concise : nous avons défini dans le code une grammaire permettant de valider les expressions de map ; et il nous a fallu seulement quelques misérables lignes de code pour le faire. C'est tout un exploit. Prenez un moment pour vous familiariser avec la définition de MapGrammar. Les grammaires sont le pilier central de Proto et une fois que vous maîtrisez les grammaires, vous serez vraiment capable de faire des choses intéressantes. En fait, toutes les choses puissantes et intéressantes que vous pouvez faire avec Proto commencez avec les grammaires.

VI - Valider des expressions suivant des grammaires

Vous vous demandez sans doute ce que nous allons faire avec MapGrammar. Proto fournit une classe de traits appeléeproto::matches pour déterminer au moment de la compilation si une expression correspond à une grammaire donnée. Nous pouvons utiliser proto::matches associé avec un static_assert du C++0x ou diverses approximations de celui-ci dans le C++03 (voir la note ci-dessous) pour mettre fin à la compilation dès qu'une expression non valide est détectée.

Avec static_assert, proto::matches et MapGrammar, nous pouvons modifier notre surcharge de la fonction format pour valider le paramètre expr avant de passer à la fonction fill_map :

1. template< class Expr >

2. std::string format( std::string fmt, Expr const & expr )

3. {

4. /* LISEZ CECI SI VOTRE COMPILATEUR ÉCHOUE SUR LA LIGNE SUIVANTE

5. *

6. * Vous avez passé à format() une expression de map invalide.

7. * Elles doivent être de la forme :

8. *      map("this", "that")("here", "there")

9. */

10. static_assert(

11. proto::matches<Expr, MapGrammar>::value

12. , "The map expression passed to format does not match MapGrammar");

13. string_map subs;

14. fill_map( expr, subs );

15. return format( fmt, subs );

16. }

Lorsque nous passons notre expression non valide dans la fonction format maintenant, notre message d'erreur va passer de plus de 50 lignes à environ 10 lignes, y compris le présent message(9) :

c:\scratch.cpp(94): error C2338: The map expression passed
to format does not match MapGrammar

Voir l'erreur complète

1>------ Build started: Project: scratch, Configuration: Debug Win32 ------
1>  scratch.cpp
1>c:\scratch.cpp(94): error C2338: The map expression passed to format does not match MapGrammar
1>          c:\scratch.cpp(109) : see reference to function template instantiation 'std::string format
<boost::proto::exprns_::expr
<Tag,Args,Arity>>(std::string,const Expr &)' being compiled
1>          with
1>          [
1>              Tag=boost::proto::tag::function,
1>              Args=boost::proto::argsns_::list3
<const boost::proto::exprns_::expr
<boost::proto::tag::function,boost::proto::argsns_::list3
<boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<map_>,0> &,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const char (&)[5]>,0>,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const wchar_t (&)[5]>,0>>,3> &,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const char (&)[5]>,0>,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const char (&)[11]>,0>>,
1>              Arity=3,
1>              Expr=boost::proto::exprns_::expr
<boost::proto::tag::function,boost::proto::argsns_::list3
<const boost::proto::exprns_::expr
<boost::proto::tag::function,boost::proto::argsns_::list3
<boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<map_>,0> &,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const char (&)[5]>,0>,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const wchar_t (&)[5]>,0>>,3> &,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const char (&)[5]>,0>,boost::proto::exprns_::expr
<boost::proto::tag::terminal,boost::proto::argsns_::term
<const char (&)[11]>,0>>,3>
1>          ]
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

Cette erreur est plus agréable parce que :

- elle est plus courte ! ;

- le message d'erreur indique ce que le problème pourrait être ;

- l'erreur apparaît en début de message et non sur une ligne au hasard, au plus profond du ventre de la bibliothèque ;

- nous avons gentiment laissé un commentaire dans assert pour que les gens sachent ce qui ne va pas quand l'assertion échoue et ce qu'il faut faire pour y remédier.

Si vous n'avez pas de compilateur C++0x avec static_assert, je recommande d'utiliser la macro BOOST_MPL_ASSERT_MSG de Boost.MPL, qui accepte une valeur booléenne évaluée au moment de la compilation et un message à afficher si le booléen est faux. L'assertion statique de la ligne 9 ci-dessus pourrait ressembler à ceci :

1. BOOST_MPL_ASSERT_MSG(

2. (proto::matches<Expr, MapGrammar>::value),

3. THE_MAP_EXPRESSION_PASSED_TO_FORMAT_DOES_NOT_MATCH_MAPGRAMMAR,

4. (MapGrammar));

Lorsque cette assertion échoue, elle émet une erreur similaire à ceci :

c:\scratch.cpp(115): error C2664: 'boost::mpl::assertion_fa
iled' : cannot convert parameter 1 from 'boost::mpl::failed
************(__thiscall format::THE_MAP_EXPRESSION_PASSED_T
O_FORMAT_DOES_NOT_MATCH_MAPGRAMMAR::* ***********)(MapGramm
ar)' to 'boost::mpl::assert::type'

VII - Erreurs à éviter

Si vous essayez l'exemple ci-dessus sur gcc-4.5, vous verrez qu'au lieu d'avoir un message d'erreur court, le static_assert retourne une erreur beaucoup plus longue !

Voir l'erreur complète

scratch.cpp: In function 'std::string format(std::string, const Expr&) [with Expr = boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<const boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<map_>, 0l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const wchar_t (&)[5]>, 0l> >, 3l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[11]>, 0l> >, 3l>, std::string = std::basic_string
<char>]':
scratch.cpp:109:55:   instantiated from here
scratch.cpp:92:9: error: static assertion failed: "The map expression passed to format does not match MapGrammar"
scratch.cpp: In function 'void fill_map(const Expr&, string_map&) [with Expr = boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<map_>, 0l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const wchar_t (&)[5]>, 0l> >, 3l>, string_map = std::map
<std::basic_string
<char>, std::basic_string
<char> >]':
scratch.cpp:61:9:   instantiated from 'void fill_map(const Expr&, string_map&) [with Expr = boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<const boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<map_>, 0l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const wchar_t (&)[5]>, 0l> >, 3l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[11]>, 0l> >, 3l>, string_map = std::map
<std::basic_string
<char>, std::basic_string
<char> >]'
scratch.cpp:101:9:   instantiated from 'std::string format(std::string, const Expr&) [with Expr = boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<const boost::proto::exprns_::expr
<boost::proto::tag::function, boost::proto::argsns_::list3
<boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<map_>, 0l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const wchar_t (&)[5]>, 0l> >, 3l>&, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[5]>, 0l>, boost::proto::exprns_::expr
<boost::proto::tag::terminal, boost::proto::argsns_::term
<const char (&)[11]>, 0l> >, 3l>, std::string = std::basic_string
<char>]'
scratch.cpp:109:55:   instantiated from here
scratch.cpp:60:9: error: invalid conversion from 'const wchar_t*' to 'char'
scratch.cpp:60:9: error:   initializing argument 1 of 'std::basic_string
<_CharT, _Traits, _Alloc>& std::basic_string
<_CharT, _Traits, _Alloc>::operator=(_CharT) [with _CharT = char, _Traits = std::char_traits
<char>, _Alloc = std::allocator
<char>, std::basic_string
<_CharT, _Traits, _Alloc> = std::basic_string
<char>]'

Que se passe-t-il dans ce cas ? Si vous vous baladez dans cet amoncellement de messages d'erreur, vous pouvez trouver le magnifique message de l'assertion statique, mais il est enterré au milieu d'un tas de déchets, avec deux autres erreurs provenant des entrailles de l'implémentation de notre EDSL. Regardons encore la nouvelle implémentation de format :

1. template< class Expr >

2. std::string format( std::string fmt, Expr const & expr )

3. {

4. /* LISEZ CECI SI VOTRE COMPILATEUR ÉCHOUE SUR LA LIGNE SUIVANTE

5. *

6. * Vous avez passé à format() une expression de map invalide.

7. * Elles doivent être de la forme :

8. *      map("this", "that")("here", "there")

9. */

10. static_assert(

11. proto::matches<Expr, MapGrammar>::value

12. , "The map expression passed to format does not match MapGrammar");

13. string_map subs;

14. fill_map( expr, subs );

15. return format( fmt, subs );

16. }

Le static_assert de la ligne 10 provoque le diagnostic correct, mais gcc garde gentiment les droits sur la compilation, pour finalement atteindre l'appel à fill_map à la ligne 14. Nous avons déjà établi que l'appel ne parviendra pas à compiler, mais personne ne dit à gcc qu'il devait s'arrêter !

En général, il ne suffit pas d'émettre un diagnostic pour les erreurs connues. Nous devons également éviter les diagnostics suivants dans les compilateurs trop zélés comme gcc. La réponse est généralement assez simple : déplacer le code dans une fonction distincte et utiliser une répartition statique pour choisir entre appeler cette fonction ou laisser le code vide, en fonction de la réussite ou de l'échec de la validation. Un peu de code devrait permettre d'être plus clair :

1. template< class Expr >

2. std::string format_impl( std::string fmt, Expr const & expr, boost::mpl::true_ )

3. {

4. string_map subs;

5. fill_map( expr, subs );

6. return format( fmt, subs );

7. }

9. template< class Expr >

10. std::string format_impl( std::string fmt, Expr const & expr, boost::mpl::false_ )

11. {

12. return std::string(); // jamais appelé pour une entrée valide

13. }

15. template< class Expr >

16. std::string format( std::string fmt, Expr const & expr )

17. {

18. /* LISEZ CECI SI VOTRE COMPILATEUR ÉCHOUE SUR LA LIGNE SUIVANTE

19. *

20. * Vous avez passé à format() une expression de map invalide.

21. * Elles doivent être de la forme :

22. *      map("this", "that")("here", "there")

23. */

24. static_assert(

25. proto::matches<Expr, MapGrammar>::value

26. , "The map expression passed to format does not match MapGrammar");

28. /* Répartir entre l'implémentation réelle ou un code vide, en fonction

29. de si les paramètres sont valides ou non.

30. */

31. return format_impl( fmt, expr, proto::matches<Expr, MapGrammar>() );

32. }

Il est ennuyeux que l'on doive modifier notre code pour accommoder à gcc comme ça, mais c'est un petit prix. Nous avons déjà fait le travail difficile de valider les paramètres. Éviter les erreurs suivantes est toujours une simple question de répartition de l'information au moment de la compilation, nous avons déjà fait les calculs de toute façon. Et pensez à vos utilisateurs, ils méritent des messages d'erreur sympathiques.

Nous avons ajouté deux surcharges d'une nouvelle fonction format_impl. La première prend un argument supplémentaire de type boost::mpl::true_ et effectue le travail demandé. La seconde prend boost::mpl::false_ et renvoie simplement une chaîne vide. La fonction format d'origine est maintenant juste une coquille vide qui (peut-être) émet un diagnostic et distribue à l'une ou l'autre des surcharges. De façon commode, proto::matches hérite de boost::mpl::true_ ou boost::mpl::false_, permettant de rendre cela possible. Avec ce changement, l'erreur complète est beaucoup plus courte :

scratch.cpp: In function 'std::string format(std::string, c
onst Expr&) [with Expr = boost::proto::exprns_::expr<boost:
:proto::tag::function, boost::proto::argsns_::list3<const b
oost::proto::exprns_::expr<boost::proto::tag::function, boo
st::proto::argsns_::list3<boost::proto::exprns_::expr<boost
::proto::tag::terminal, boost::proto::argsns_::term<map_>,
0l>&, boost::proto::exprns_::expr<boost::proto::tag::termin
al, boost::proto::argsns_::term<const char (&)[5]>, 0l>, bo
ost::proto::exprns_::expr<boost::proto::tag::terminal, boos
t::proto::argsns_::term<const wchar_t (&)[5]>, 0l> >, 3l>&,
boost::proto::exprns_::expr<boost::proto::tag::terminal, bo
ost::proto::argsns_::term<const char (&)[5]>, 0l>, boost::p
roto::exprns_::expr<boost::proto::tag::terminal, boost::pro
to::argsns_::term<const char (&)[11]>, 0l> >, 3l>, std::str
ing = std::basic_string<char>]':
scratch.cpp:126:55:   instantiated from here
scratch.cpp:112:9: error: static assertion failed: "The map
expression passed to format does not match MapGrammar"

VIII - Conclusions et ce qui va venir ensuite

Merci de m'avoir lu. Puisque j'ai commencé à vous demander d'être des auteurs de bibliothèques, je me sens obligé de vous donner les outils pour rendre vos bibliothèques conviviales. Comme vous pouvez le voir, nous devons être un peu proactifs pour que notre code se comporte correctement lors de la collecte des déchets, mais ça n'était pas si difficile. Bien que j'aie surtout parlé des EDSL et de Proto, ces techniques sont applicables plus largement :

1. Valider les paramètres des templates au niveau des limites de l'interface ;

2. Utiliser static_assert du C++0x ou un équivalent dans le C++03 pour générer des diagnostics lisibles ;

3. Donner des commentaires détaillés avec les assertions statiques pour que les gens sachent ce qui s'est mal passé et comment y remédier ;

4. Récupérer les entrées invalides pour éviter les erreurs suivantes.

Ces techniques peuvent réduire considérablement la quantité de messages que le compilateur envoie aux développeurs C++ lors des utilisations quotidiennes.

Les grammaires de Proto permettent de valider facilement et de (oserais-je le dire ?) façon amusante les arbres d'expressions. Mais ils sont beaucoup plus utiles que cela. Vous pouvez utiliser les grammaires de Proto pour limiter les surcharges d'opérateur de Proto uniquement à ceux qui créent des arbres valides. Et en intégrant des actions sémantiques (c'est-à-dire des transformations) au sein des grammaires de Proto, vous pouvez écrire des algorithmes qui manipulent des arbres et générer du code de manière très puissante. Dans les prochains articles, nous allons approfondir les grammaires de Proto et les transformations. Mais avant ça, nous allons regarder de plus près les expressions de Proto et comment les étendre et comment ajouter et personnaliser les fonctions membres, pour en faire autre chose que de stupides arbres statiques.

Jusqu'à la prochaine fois, n'oubliez pas de valider vos paramètres. Et si vous voyez la moindre erreur de template, déposez un « bug » !

IX - Remerciements

Merci à Eric Niebler de nous avoir autorisés à traduire Expressive C++: Why Template Errors Suck and What You Can Do About It.

Merci à Flob90 pour son aide pour la traduction et sa relecture attentive. Merci à ClaudeLELOUP pour sa relecture orthographique.

(1)Les articles précédents de cette série font référence à l'acronyme « DSEL ». En raison de commentaires que j'ai reçus, j'ai décidé de passer au terme plus couramment utilisé « EDSL ». Je continue de parler de la même chose.

(2)Une référence connue au métavers fictionnel de Neal Stephenson dans son livre « Snow Crash », un de mes favoris.

(3)Boost.StaticAssert et Boost.Concept_check, par exemple.

(4)Compilé avec g++ 4.5.0 avec le dépôt 'trunk' de Boost, le 16 septembre 2010.

(5)Je suis désolé de m'en prendre aux auteurs de Spirit ici, mais je devais choisir quelqu'un. Spirit est en fait bien meilleur que les autres pour récupérer et reporter les entrées invalides et trouver cette faille particulière dans l'armure de Spirit, c'est un peu du chipotage.

(6)Je ne prétends pas que Boost.Proto est une solution miracle. Il fournit des outils. Il faut les utiliser. Boost.Spirit est en réalité implémenté avec Proto, mais ça n'aide pas dans cet exemple. Il semble que Spirit continue avec cette entrée invalide et se plante au plus profond des instanciations de templates.

(7)« Mad Libs » est une marque déposée de Penguin Group (USA) Inc.

(8)Testé avec Microsoft Visual C++ 2010.

(9)Testé avec Microsoft Visual C++ 2010.