Journal Switch, chaîne constante et c++

Posté par (page perso) . Licence CC by-sa
Tags :
16
31
août
2016

Salut 'Nal (ah non, ça marche pas)

Lors d'une discussion avec un collègue, ça chambrait 1 gentillement sur l'absence de switch sur des chaînes constantes en C++. Ça a déclenché quelques recherches de mon côté, et voilà ce que j'ai pu trouver (les codes qui suivent ne sont que le reflet de ce que j'ai pu lire en ligne, rien de nouveau sous le soleil brestois)

Approche LLVM : StringSwitch

Source : http://llvm.org/docs/doxygen/html/StringSwitch_8h_source.html

L'idée est de simuler une cascade de

if(str == "str0")
    return value0;
else if(str == "str1")
    return value1;
else
    return value2;

La solution est plutôt amusante, je vous mets une version simplifiée ci-dessous

template<typename Holder>
class StringSwitch {
  Holder const * holder_;
  std::string const value_;

  public:
  StringSwitch(std::string value) : holder_(nullptr), value_(value) {}

  template<size_t N>
  StringSwitch& Case(char const (&v)[N], Holder const& h) {
    if(!holder_ && value_.size() == N- 1 && value_ == v)
      holder_ = &h;
    return *this;
  }
  Holder const& Default(Holder const& h) {
    if(holder_) return *holder_;
    return h;
  }
};

int main(int argc, char **argv) {
  auto && msg =
    StringSwitch<std::string>(argc == 1 ? "--help" : argv[1])
    .Case("hello", "world")
    .Case("--help", "no help can be found in this world")
    .Default("");
  if(msg.empty())
    return 1;
  else {
    std::cout << msg << std::endl;
    return 0;
  }
}

Ça coûte à peine plus que la cascade de if (quelques comparaison à un pointeur nul en plus) et c'est plutôt élégant. Bien sûr ce n'est qu'un petit exemple, il faut se référer au code complet pour saisir les subtilités !

Pour réduire un peu le coût de comparaison (on en \mathcal{O}(n \times m) dans le pire des cas, où n est le nombre de case et m la taille moyenne d'une chaîne, on pourrait utiliser une std::map que l'on marquerait static const. Malheureusement, le constructeur de std::map n'est pas constexpr, donc on aurait une initialisation à l'exécution. On pourrait, pour un compromis différent, utiliser une std::unordered_map aussi.

Et pour les chagrins qui veulent du code dans leur case: on peut utiliser une std::function (mais il y a un coût caché). Un truc du genre :

#include <map>
#include <functional>

int main(int argc, char **argv) {
  static const
    std::map<std::string, std::function<int()>> Switch = {
      {"hello", 
        []() {
          std::cout << "world" << std::endl;
          return 1;
        }
      },
      {"--help",
        []() {
          std::cout << "no help can be found in this world" << std::endl;
          return 1;
        }
      }
    };

  auto where = Switch.find(argc == 1 ? "--help" : argv[1]);
  return where == Switch.end() ? 1 : where->second();
}

Approche C++14

Cette approche est fortement inspirée de ce post https://dev.krzaq.cc/post/switch-on-strings-with-c11/

L'idée est de reposer sur les constexpr pour calculer un hash à compile time, et donc traduire une chaîne en un entier, valide pour un switch. Un peu de sucre syntaxique avec les user defined literals et on obtient ça :

constexpr unsigned long hash(char const* str) {
  unsigned long hash = 5381;
  int c = 0;

  while (c = *str++)
    hash = hash * 33 + c;

  return hash;
}

constexpr auto operator ""_h(char const str[], size_t) {
  return hash(str);
}


int main(int argc, char **argv) {
  switch(hash(argc == 1 ? "--help" : argv[1])) {
  case "hello"_h:
    std::cout << "world" << std::endl;
    return 0;
  case "--help"_h:
    std::cout << "no help can be found in this world" << std::endl;
    return 0;
  default:
    return 1;
  };
}

Malheureusement l'implémentation de std::hash n'est pas constexpr, sinon on aurait pu éviter de fournir une implem miteuse2 mais respectant la constexpritude.

On est pas loin du switch sur des litéraux, et ça coûte un calcul de hash + le saut.

Conclusion

Comme souvent, on a le choix en C++, et c'est ce que j'aime dans ce langage (et ce qui en fait un langage difficile à utiliser en production). Mais la monotonie n'est pas prête de venir toquer à la porte !


  1. petit conseil : si ça chambre, aère ! 

  2. http://www.pokepedia.fr/Mimitoss 

  • # Plusieurs remarques

    Posté par (page perso) . Évalué à 1. Dernière modification le 31/08/16 à 11:41.

    1/ Ne jamais réinventer des hashes (sans une très bonne raison), la hash que tu utilises est très faible point de vue collision. Les siphash sont un meilleur choix, même si non parfait.
    2/ Ce n'est pas sans raison que ce n'est pas dans le langage : il n'y a pas qu'une seule solution, et aucune, n'est la solution à tous les cas. Il y a les trie, ou simplement mettre les strings dans une hash table (clef) et mettre une fonction en valeur…

    • [^] # Re: Plusieurs remarques

      Posté par (page perso) . Évalué à 9.

      1/ Ne jamais réinventer des hashes (sans une très bonne raison),

      Tout à fait d'accord avec toi; C'est même écrit dans le journal :-/

      […] on aurait pu éviter de fournir une implem miteuse

      ou simplement mettre les strings dans une hash table (clef) et mettre une fonction en valeur
      il n'y a pas qu'une seule solution

      Tout à fait d'accord avec toi; C'est même écrit dans le journal :-/

      Et pour les chagrins qui veulent du code dans leur case: on peut utiliser une std::function

      Avec un code qui illustre ça

  • # Vérification de l'égalité?

    Posté par (page perso) . Évalué à 10. Dernière modification le 31/08/16 à 12:19.

    Étant donné qu'une fonction de hash (ici: String → int) n'est pas injective, il faut vérifier l'égalité de chaînes même dans le cas où elles ont le même hash. Où est-elle effectuée dans ton dernier exemple?

    Le switch sur les Strings en Java 7 calcule aussi le hash, mais c'est juste une question d'optimisation (random link) ; évidemment cela ne permet pas de se passer de la vérification de l'égalité.

    blog.rom1v.com

  • # Map à la place du swicth/case

    Posté par . Évalué à 7.

    Et pour les chagrins qui veulent du code dans leur case: on peut utiliser une std::function (mais il y a un coût caché). Un truc du genre :

    C'est ce que je fais de plus en plus (alors que mon langage a un switch sur les java.lang.String ;) ). Je vois de plus en plus de problèmes aux switch/case :

    • ils alourdissent fortement le code
    • chaque case leak son contexte avec le reste du code
    • ils sont piégeux (qui n'a pas un jour oublié le break ?)
    • il est difficile de tester chaque case
    • il est possible d'ajouter des fonctions mais ça ne rend pas le code énormément plus simple
    • lorsqu'il s'agit juste de choisir une valeur (encore plus que lorsqu'il s'agit d'un calcul), on voudrait pouvoir être totalement déclaratif ('a' → 12, 'b' → 642, 'r' → 727,…)

    Du coup pour toutes ces raisons j'ai une lourde préférence pour utiliser une Map. Je la déclare et construit en statique à la classe, donc le coût de construction n'est payé d'une seule fois par exécution et tant que je n'ai pas de vrai besoin de performance (et que l'on a pas encore établi que les problème venait de là) je garde le tout sous cette forme.

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Map à la place du swicth/case

      Posté par (page perso) . Évalué à 4. Dernière modification le 31/08/16 à 14:05.

      alors que mon langage a un switch sur les java.lang.String

      chaque case leak son contexte avec le reste du code

      Éventuellement tu peux scoper :

      switch (id) {
          case 1: {
              int i = 42;
              // …
              break;
          }
          case 2: {
              int i = -12;
              // …
              break;
          }
      }

      lorsqu'il s'agit juste de choisir une valeur (encore plus que lorsqu'il s'agit d'un calcul), on voudrait pouvoir être totalement déclaratif ('a' → 12, 'b' → 642, 'r' → 727,…)

      Ce pattern n'est pas si mal adapté aux switches :

      int map(char value) {
          switch (value) {
          case 'a': return 12;
          case 'b': return 642;
          case 'r': return 727;
          // …
          }
      }

      En java en plus dans une Map, tu vas devoir wrapper ces valeurs dans des objets.

      blog.rom1v.com

      • [^] # Re: Map à la place du swicth/case

        Posté par . Évalué à 3.

        Éventuellement tu peux scoper :

        Ça résous que partiellement, tu n'a pas vraiment de lisibilité sur quels sont les données d'entrée et de sortie de chaque cas.

        Ce pattern n'est pas si mal adapté aux switches :

        int map(char value) {
            switch (value) {
            case 'a': return 12;
            case 'b': return 642;
            case 'r': return 727;
            // …
            }
        }

        Oui c'est une option (c'est aussi une solution pour ce dont je parle au dessus). Mais vraiment je préfère une construction comme ici (avec guava) :

        public static final Map<String, Integer> myStruc = ImmutableMap.<String, Integer>builder()
                   .put("foo", 42)
                   .put("bar", 84)
                   .build(); // on peut utiliser .of() dans ce cas c'est plus concis

        On peut plus tard imaginer charger ces valeurs depuis un fichier.

        En java en plus dans une Map, tu vas devoir wrapper ces valeurs dans des objets.

        J'avoue ne pas connaître le coût de l'auo(un)boxing. Tu as plus d'info là dessus ? (je sais qu'il y a des systèmes de cache dans la JVM pour certaines valeurs par exemple)

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: Map à la place du swicth/case

          Posté par (page perso) . Évalué à 3. Dernière modification le 31/08/16 à 14:59.

          J'avoue ne pas connaître le coût de l'auo(un)boxing.

          Les objets, ça prend de la place. Les HashMaps en particulier :)

          Cf Android Memories à partir de la slide 24.

          (je sais qu'il y a des systèmes de cache dans la JVM pour certaines valeurs par exemple)

          Oui, par exemple l'autoboxing des int utilise un cache pour les valeurs sur 8 bits. Cf l'implémentation de Integer.valueOf(int) (qui est appelé automatiquement pour l'autoboxing).

          Mais vraiment je préfère une construction comme ici (avec guava)

             public static final Map<String, Integer> myStruc = ImmutableMap.<String, Integer>builder()
                        .put("foo", 42)
                        .put("bar", 84)
                        .build(); // on peut utiliser .of() dans ce cas c'est plus concis

          Perso je trouve ça très verbeux.

          En C++ c'est plus joli :

              const std::map<char, int> map {
                  { 'a', 12 },
                  { 'b', 642 },
                  { 'r', 727 }
              };

          blog.rom1v.com

  • # Lisibilité du code

    Posté par (page perso) . Évalué à 10.

    La solution est plutôt amusante, je vous mets une version simplifiée ci-dessous

    Ben heureusement que c'était la version simplifiée ;-)

    Bon j'imagine bien qu'ici il s'agit plus d'un exercice de style que la recherche d'une réelle solution à mettre en production.

    D'un point de vue lisibilité du code, je trouve personnellement que :

    • Cascade de if/else : il me faut moins d'une seconde pour reconnaître la structure du code, même si c'est un peu moche car répétition du « else if (str == ».
    • La solution soit disant « élégante » avec les templates : je n'ose imaginer le temps que ça me prendrait pour me rendre compte de quoi il s'agit… Heureusement que je savais qu'on essayait de reproduire une cascade de if/else.
    • std::function : c'est mieux que la précédente (templates), mais ce n'est pas plus beau que les if/else (il y a tout plein de symboles qui ne rendent pas les choses plus lisibles à mon sens). Je pense qu'il me faut ~10 secondes pour être certain de bien interpréter (comprendre : qu'il n'y a pas un piège subtile qui se cache derrière cette conception).
    • Approche C++14 : ça commence à ressembler à quelque chose de lisible (je comprends vite la structure), mais on dévoile ici des choses de bas niveau (comme la comparaison de hash) qui polluent un peu la lecture.

    Finalement, je crois que je préfère la version if/else, qui même moche, me semble plus expressive car moins polluée par des détails bas niveau du langage (le concept de switch/case c'est quand même du niveau très bas, et là ça donne une impression de vouloir inventer une roue de manière compliquée).

    J'aimais beaucoup le C++ quand j'étais jeune, mais depuis j'accorde de plus en plus d'importance à la lisibilité et le C++ me paraît de plus en plus compliqué de ce point de vue là. Parfois, j'aurais plus envie de le classer comme un langage d'artiste que comme un langage industriel. Bien sûr, on peu se restreindre à faire du code assez simple, mais j'ai toujours peur de devoir repasser derrière un artiste x_x

    • [^] # Re: Lisibilité du code

      Posté par . Évalué à 4.

      Cascade de if/else : il me faut moins d'une seconde pour reconnaître la structure du code, même si c'est un peu moche car répétition du « else if (str == ».

      Personnellement il m'en faut bien plus (quelque soit le langage) pour vérifier que l'on fait un test d'égalité sur la même variable et qu'on a pas des doublons… Le if/else laisse toute l'amplitude a faire tout et n'importe quoi ce que je trouve très gênant. Personnellement je considère la multiplication de if dans un code comme un code smell donc je préfère éviter dans ajouter.

      La solution soit disant « élégante » avec les templates : je n'ose imaginer le temps que ça me prendrait pour me rendre compte de quoi il s'agit… Heureusement que je savais qu'on essayait de reproduire une cascade de if/else.

      Question d'habitude. Quand tu commence à jouer avec des API fluent, ça se lit vite.

      std::function : c'est mieux que la précédente (templates), mais ce n'est pas plus beau que les if/else (il y a tout plein de symboles qui ne rendent pas les choses plus lisibles à mon sens). Je pense qu'il me faut ~10 secondes pour être certain de bien interpréter (comprendre : qu'il n'y a pas un piège subtile qui se cache derrière cette conception).

      Là AMHA le problème vient des lambdas du C++ qui sont trop complexes.

      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Lisibilité du code

        Posté par (page perso) . Évalué à 4.

        Question d'habitude. Quand tu commence à jouer avec des API fluent, ça se lit vite.

        En fait l’implémentation est complexe, mais l'usage est relativement simple et lisible. De mon point de vue il n'est pas forcement nécessaire de comprendre l’implémentation d'une abstraction pour l'utiliser correctement.

        Ainsi j'aime bien l'approche StringSwitch qui est très claire (à mon sens). Je trouve que l'approche à base de std::map ne devrait pas exposer la std::map directement ni le find. Je ferais un truc du genre : (Api non testé).

        template<typename Result, typename Value>
        Result Switch(Value v, std::map<Value, std::function<Result()>>);
  • # Pas uniquement string

    Posté par (page perso) . Évalué à 3. Dernière modification le 31/08/16 à 17:15.

    Ça a quelques années déja, mais Bjarne Stroustrup avait publié à l'époque une petite lib permettant de faire du pattern matching "à l'OCaml" en C++.

    Donc j'imagine également du "switch/Case" avec des string. ( j'imagine car je n'ai jamais joué avec )

    https://github.com/solodon4/Mach7

    Si tu veux t'amuser avec.

    • [^] # Re: Pas uniquement string

      Posté par (page perso) . Évalué à 2.

      C'est pas clair à quel point c'est du C++ ou à quel point c'est une extension au standard actuel. La syntaxe est tout de même vraiment répugnante.

      Avec d'autres langages, le pattern matching c'est extrêmement puissant, concis et clair et cela fait partie des outils qu'il me manque dramatiquement quand je fais du C++.

      • [^] # Re: Pas uniquement string

        Posté par (page perso) . Évalué à 1.

        C'est pas clair à quel point c'est du C++ ou à quel point c'est une extension au standard actuel.

        C'est du pure C++ sauf erreur de ma part.

        La syntaxe est tout de même vraiment répugnante.

        Question de goût. Je trouve la syntaxe au contraire très clair pour du code templaté.
        Par contre comme tout comme templaté assez poussé, je suis curieux de la taille des messages d'erreurs à la compilations

        Avec d'autres langages, le pattern matching c'est extrêmement puissant, concis et clair et cela fait partie des outils qu'il me manque dramatiquement quand je fais du C++.

        Je ne suis on ne peut plus d'accord. C'est au programme du comité de normalisation C++, donc espérons.

        • [^] # Re: Pas uniquement string

          Posté par (page perso) . Évalué à 2.

          Question de goût. Je trouve la syntaxe au contraire très clair pour du code templaté.

          La dessus, rien à dire ;) Mais bon, j'ai un peu pris l'habitude d'Haskell (ou autre). Pour le troll, comparons :

          int fact(int n)
          {
              var<int> m;
          
              Match(n)
              {
                When(0)     return 1;
                When(1)     return 1;
                When(_)     return n * fact(n-1);
              }
              EndMatch
          }
          fact n = case n of
             0 -> 1
             1 -> 1
             _ -> n * fact (n - 1)
          
          -- ou
          
          fact 0 = 1
          fact 1 = 1
          fact n = n * fact (n - 1)

          Vachement plus compact, lisible et polymorphique ;) (mais bon, je troll ;)

      • [^] # Re: Pas uniquement string

        Posté par (page perso) . Évalué à 2.

        Je m'auto répond. Un point qui sent vraiment mauvais dans cette approche c'est que pour stocker le résultat d'un match, il faut déclarer une variable non initialisée, exemple :

        int fib2(int n)
        {
            var<int> m;
        
            Match(n)
            {
              When(1)     return 1;
              When(2)     return 1;
              When(2*m)   return sqr(fib2(m+1)) - sqr(fib2(m-1));
              When(2*m+1) return sqr(fib2(m+1)) + sqr(fib2(m));
            }
            EndMatch
        }

        Ce qui veut dire que on peut se servir d'une variable mal initialisée dans une branche. J'ai tendance à ne plus supporter çà et à refuser les revues de code qui en contiennent.

        • [^] # Re: Pas uniquement string

          Posté par (page perso) . Évalué à 2.

          En C++, var<int> m; est default-initialized.
          Pour les types primitifs, ça veut dire non initialisé, mais pour un type complexe pas forcément.

          #include <iostream>
          
          template <typename T>
          struct var {
              T value;
              var() : value(42) {}
          };
          
          int main() {
              var<int> m;
              std::cout << m.value << std::endl;
              return 0;
          }

          blog.rom1v.com

          • [^] # Re: Pas uniquement string

            Posté par (page perso) . Évalué à 2.

            En C++, var m; est default-initialized.

            C'est comme dire non initialisé, mais en pire ;)

            Autant cela à un sens plus ou moins arbitraire pour certains types (genre std::vector), mais pourquoi un bool serait plus false que true ou un int mérite-t-il plus d'être 0 que 30. Tant qu'on est dans le code-smell, j'ai tendance à faire des bonds sur les classes qui proposent un constructeur par défaut vide avec des valeurs par défaut. Il faut vraiment justifier que le choix par défaut à du sens.

            Généralement je préfère une fonction static ou free avec un nom explicite pour les cas par défaut.

            • [^] # Re: Pas uniquement string

              Posté par . Évalué à 0. Dernière modification le 01/09/16 à 21:17.

              Parce que dans le cas géneral une variable non initialisée a de fortes chances de se retrouver avec tous les bits à zero car l'endroit en mémoire ou elle est allouée est à zero (tant que c'est pas sur la stack) ?

              • [^] # Re: Pas uniquement string

                Posté par (page perso) . Évalué à 0.

                Ma question n'était pas la raison technique qui fait que c'est plutôt des bits à zéro qu'autre chose. Je voulais plutôt discuter du coté totalement arbitraire des initialisation par défaut.

                Les valeur par défaut sont, à mon sens, plus dangereuses que les valeur non-initialisée. Parce que non-initialisée, cela fait souvent n'importe quoi, et dans le meilleur des cas, cela plante, et tu trouves vite le bug et au moins tu obtient un comportement instable qui t'invites à chercher une erreur. Mais lorsque c'est initialisé par défaut, tu vas obtenir un comportement stable, souvent cohérent, mais qui est totalement faux.

                De plus, les valeurs par défaut des types primitifs sont assez évidentes, mais celles de types plus élaborés sont totalement dépendants des choix des auteurs de la classe, et cela rend le code bien moins lisible. Il faut aussi voir que les valeurs par défaut peuvent changer au bon vouloir du l'auteur de la classe, et celui-ci peut penser que son changement est indolore alors qu'en fait il détruit ton code.

                • [^] # Re: Pas uniquement string

                  Posté par . Évalué à 4.

                  Les valeur par défaut sont, à mon sens, plus dangereuses que les valeur non-initialisée.

                  Ca peut être ton sens, mais en pratique ce n'est absolument pas le cas.

                  L'exemple flagrant est les pointeurs, notamment pointeurs de fonction. Si tu le garde non-initialisé tu as un énorme trou de sécurité potentiel.

                  Ensuite, il n'y a absolument aucun besoin d'avoir un code instable pour aller chercher ces erreurs, n'importe quel compilateur décent te détectera une variable non-initialisée sur demande

                  De plus, les valeurs par défaut des types primitifs sont assez évidentes, mais celles de types plus élaborés sont totalement dépendants des choix des auteurs de la classe, et cela rend le code bien moins lisible.

                  Gni ? Tu demandes quoi là ? Evidemment que l'auteur de la classe va mettre ce qu'il désire dans le constructeur par défaut, c'est son code ! Tu veux faire quoi à la place ?

                  • [^] # Re: Pas uniquement string

                    Posté par (page perso) . Évalué à 1.

                    L'exemple flagrant est les pointeurs, notamment pointeurs de fonction. Si tu le garde non-initialisé tu as un énorme trou de sécurité potentiel.

                    C'est quoi la valeur par défaut d'un pointeur de fonction qui as du sens ? nullptr, ou une fonction au hasard ? Les deux cas n'ont pas de sens et sont dangereux, bref, avoir un pointeur "default-initialised" n'as pas de sens.

                    Ensuite, il n'y a absolument aucun besoin d'avoir un code instable pour aller chercher ces erreurs, n'importe quel compilateur décent te détectera une variable non-initialisée sur demande.

                    Justement ;) : les valeurs non-initialisées sont moins dangereuses que les valeurs initialisée par défaut, car souvent les valeurs non initialisée vont te générer une erreur, alors que les valeurs par défaut ne vont pas t'en générer et tu continues sur un bout de code qui n'a plus de sens.

                    On parle bien du cas tu as un bug dans ton code et alors que tu pensais gérer tous les cas de figure, il en manque certains et au lieu de mettre une valeur qui a du sens dans ton code, tu finis avec une valeur par défaut qui n'en a pas.

                    Gni ? Tu demandes quoi là ? Evidemment que l'auteur de la classe va mettre ce qu'il désire dans le constructeur par défaut, c'est son code ! Tu veux faire quoi à la place ?

                    Je ne veux pas de constructeur par défaut ! Exemple, si tu croises le bout de code suivant Color c;, quel est la valeur d'une Color ? Maintenant tu croises Color c = Color::Black(). Cette seconde solution est :

                    a) Bien plus facile à lire / comprendre. Il est plus explicite. Il demande aussi moins de surcharge intellectuel au développeur qui n'a pas à retenir la liste des cas par défaut pour toutes les classes.
                    b) Il est moins sujet aux changements des comportements par défaut, je détail ce point qui te faisait bondir.

                    Imagine une interface de la classe Color comme suit :

                    // Color Triple
                    class Color
                    {
                    ....
                    public:
                        // Default Color (black)
                        Color();
                    
                        // Init the triple with (a, b, c)
                        Color(const float a, const float b, const float c);
                    };

                    Ici mon seul contrat concernant le constructeur par défaut c'est le petit commentaire qui dit "black". C'est faible, et demain ce constructeur par défaut peut changer très facilement pour construire une autre couleur, sans doute le blanc, car pour les besoins de ce projet on fait plus du multiplicatif sur les couleurs que de l'additif, donc le blanc semble un meilleur choix par défaut. Tu peux aussi imaginer un refactoring dans lequel on change la classe de gestion de couleur, la plupart des opérations ont la même sémantique, sauf ? Les choix arbitraire des constructeurs par défaut.

                    Une autre conception pourrait donner :

                    // Color Triple
                    class Color
                    {
                    ....
                    public:
                        // Smart constructors
                        static Color Black();
                        static Color White();
                    
                        // Init the triple with (a, b, c)
                        Color(const float a, const float b, const float c);
                    };

                    Dans ce cas là, pas de constructeur par défaut, et des constructeurs aux noms bien plus explicites et plus plus robustes, ce n'est pas une valeur par défaut arbitraire, c'est Black ou White, c'est écrit en dur dans le nom de la fonction, donc il faudrait vraiment être pervers pour faire un refactoring dans lequel la nouvelle classe Color renvoie du vert dans sa fonction White.

                    En résumé, je préfère l'explicite et j'évite les les valeurs par défauts, les constructeurs par défauts, les arguments par défauts sur les fonction. Tout cela parce que de mon expérience, c'est trop facile de se tromper, et cela coûte trop cher. J'évite aussi les variables non initialisées (ou initialisées par défaut) qui servent de valeur de sortie d'une fonction en passage par référence, parce que c'est encore un coup à se planter. Je veux qu'à tout moment dans mon code, si j'ai accès à une variable, elle ai un sens.

                    • [^] # Re: Pas uniquement string

                      Posté par (page perso) . Évalué à 4.

                      C'est quoi la valeur par défaut d'un pointeur de fonction qui as du sens ? nullptr, ou une fonction au hasard ? Les deux cas n'ont pas de sens et sont dangereux, bref, avoir un pointeur "default-initialised" n'as pas de sens.

                      L'initialisation par défaut a du sens et c'est évident. Je sais que les programmeurs Haskell adore rappeler au autres à quel point leur langage est supérieur à la plèbe de ce monde car il n'autorise pas l'initialisation, mais la tu fais preuve d'un poil de mauvaise fois :)

                      Un dangling pointer va te causer des effets de bords alétoire, dépendant de la platforme et de l'initialisation de la mémoire. Dans le cas de l'utilisation d'un pointeur non initialisé, tu peux trés bien obtenir un programme qui marchera pendant 10 ans car ton pointeur utilise une région de mémoire qui est aléatoirement remplit de zero, et qui va segfaulter aleatoirement aprés dix ans car pour une raison X, ce segment de mémoire était différent ce jour là.

                      Dans le cas d'une initialisation forcée à null_ptr, tu auras un comportement déterministique ( qui segfaultera ) ce qui est déja bien plus "safe".

                      L'initialisation par défaut doit TOUJOURS être fait, sauf si trés bonne raison, point barre.

                      Ici mon seul contrat concernant le constructeur par défaut c'est le petit commentaire qui dit "black". C'est faible, et demain ce constructeur par défaut peut changer très facilement pour construire une autre couleur, sans doute le blanc, car pour les besoins de ce projet on fait plus du multiplicatif sur les couleurs que de l'additif, donc le blanc semble un meilleur choix par défaut.

                      Dans ce cas, ton problème est ton développeur qui trouve bon de changer le comportement par défaut d'une fonction sans changer l'API. Ce qui est idiotique et inacceptable dans tout code partagé. Le problème ce n'est pas l'initialisation par défaut, encore une fois.

                      • [^] # Re: Pas uniquement string

                        Posté par . Évalué à 2.

                        Dans le cas d'une initialisation forcée à null_ptr, tu auras un comportement déterministique ( qui segfaultera ) ce qui est déja bien plus "safe".

                        C'est un comportement non défini en C, occasionnellement ça peut faire des choses super étranges…
                        http://stackoverflow.com/questions/22847539/does-dereference-a-null-pointer-guarantee-to-crash-a-program-in-c-c

                      • [^] # Re: Pas uniquement string

                        Posté par . Évalué à 4.

                        L'initialisation par défaut doit TOUJOURS être fait, sauf si trés bonne raison, point barre.

                        Vous n'avez pas l'air de vous comprendre…

                        Il ne veut pas d'initialisation automatique silencieuse, il veut que celui qui crée l'objet se pose la question de la valeur que cet objet doit avoir.

                        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                        • [^] # Re: Pas uniquement string

                          Posté par . Évalué à 2.

                          Pour ça, il suffit de déclencher une exception dans le constructeur par défaut, non ?

                          • [^] # Re: Pas uniquement string

                            Posté par . Évalué à 3.

                            Personne n'a dit que c'était impossible :)

                            Si c'est pour empêcher ce genre de choses, je préfère par contre déclarer le constructeurs comme privé comme ça c'est vérifié systématiquement à la compilation.

                            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                          • [^] # Re: Pas uniquement string

                            Posté par . Évalué à 3.

                            Non. Le but c'est que le code ne compile pas si une variable peut ne pas être initialisée au moment où elle utilisée.
                            Swift fait ca notamment. Pas de valeurs par défaut, tu doit faire un choix explicite avant d'utiliser la variable (que ca soit une locale, un field ou quoi que ce soit d'autre).

                            Pour null, les optionals, c'est bon, mangez en :)

                            Linuxfr, le portail francais du logiciel libre et du neo nazisme.

                      • [^] # Re: Pas uniquement string

                        Posté par (page perso) . Évalué à 4.

                        L'initialisation par défaut a du sens et c'est évident. Je sais que les programmeurs Haskell adore rappeler au autres à quel point leur langage est supérieur à la plèbe de ce monde car il n'autorise pas l'initialisation, mais la tu fais preuve d'un poil de mauvaise fois :)

                        Désolé, mon commentaire était sans doute puant à la mode élitiste Haskell, ce n'était pas mon but et je me suis mal exprimé.

                        Je communique mon intérêt pour Haskell, et que ce langage influence beaucoup ma façon de travailler, mais au final le C++ reste mon outil de travail et d'enseignement. C'est un langage que j'apprécie, malgré ses défauts. Il me permet de faire mon travail alors que je ne pourrais pas le faire en Haskell pour des raisons de performance.

                        Ai-je tort d'essayer d'appliquer des principes inspirés d'autres langages pour rendre mon code C++ plus robuste ? Ai-je tort de critiquer dans un but d'amélioration, un langage que j'utilise tous les jours? Par exemple, je suis très heureux de l'arrivée des std::optional en C++17, mais je trouve que l'api proposée est un échec total puisqu'elle permet beaucoup d'erreurs qu'une api différente aurait évitée et à ce niveau ils auraient pu s'inspirer d'Haskell (ou de Rust, ou de OCaml, …). Au final, je ne me servirais des std::optional qu'une fois enrobés dans une API plus sécurisée.

                        Pour le reste, d'autres ont déjà montrés qu'un pointeur initialisé à null par défaut est potentiellement un comportement indéfini, et dans ce cas tout aussi dangereux qu'un pointeur non initialisé. Mais mon point n'était pas que je conseil de privilégier la non initialisation à l'initialisation par défaut. Je veux juste bannir les deux autant que possible et je pense qu'il est possible de construire des API qui apportent cette sécurité.

                        Pour finir, je préfère les valeurs explicites car je pense que cela augmente la lisibilité du code. Et à ce niveau, les constructeurs par défaut ne sont pas explicite dans leur intentions. Encore une fois, quel doit être le constructeur par défaut d'une Matrice 4x4 ? d'une couleur ? d'un répertoire ? d'une image ? Je ne sais pas répondre à ces questions, et c'est pour cela que je préfère des fonctions / méthodes statiques avec des noms explicites que des constructeurs dont le comportement est arbitraire et implicite.

                    • [^] # Re: Pas uniquement string

                      Posté par . Évalué à 3.

                      Qu'on se comprenne.

                      Le fait que les variables non-initialisées sont bcp plus dangereuses que les variables initialisées à une valeur par défaut est un fait établi. Ce n'est pas un cas d'opinion.

                      C'est quoi la valeur par défaut d'un pointeur de fonction qui as du sens ? nullptr, ou une fonction au hasard ? Les deux cas n'ont pas de sens et sont dangereux, bref, avoir un pointeur "default-initialised" n'as pas de sens.

                      nullptr est la bonne valeur, et la raison est super simple :

                      • nullptr assure que si la fonction est appelée ton soft crashe (accès à la page d'addresse 0 qui n'est pas mappée --> aucune exécution de code) sans pouvoir permettre d'exécution de code non controllée
                      • laisser le pointeur non initiliasé permet à un attaquant de manipuler le soft jusqu'à ce qu'il ait la valeur qu'il veut au bon endroit (sur la stack en causant certains appels à être fait pour mettre les valeurs qu'il veut sur la stack, en mémoire avec un heap spray) et lui permettre ainsi d'exécuter du code arbitraire

                      Bref tu peux penser ce que tu veux, mais si tu oses aller donner ton explication à n'importe quelle personne en sécurité (moi par exemple, qui a passé les 12 dernières années dans le milieu) tu vas leur filer un arrèt cardiaque tellement ton explication ne tient absolument pas la route et démontre un manque de compréhension des enjeux.

                      • [^] # Re: Pas uniquement string

                        Posté par (page perso) . Évalué à 3. Dernière modification le 02/09/16 à 21:33.

                        nullptr assure que si la fonction est appelée ton soft crashe (accès à la page d'addresse 0 qui n'est pas mappée --> aucune exécution de code) sans pouvoir permettre d'exécution de code non controllée

                        En fait, pour être précis, déréférencer un pointeur null ne garantit pas que le soft crashe : c'est un comportement indéfini (undefined behavior).

                        Par exemple, compile ce code avec gcc -O2 et exécute-le :

                        #include <stdio.h>
                        #include <malloc.h>
                        
                        int main(int argc, char *argv[]) {
                            int *i = argc == 1 ? NULL : malloc(sizeof(int));
                            *i = 42;
                            if (!i)
                                return 1;
                            printf("pwnd %d\n", *i);
                            return 0;
                        }

                        (tiré d'un billet que j'ai écrit il y a quelque temps)

                        blog.rom1v.com

                        • [^] # Re: Pas uniquement string

                          Posté par (page perso) . Évalué à 2.

                          (si ça segfault avec gcc -02, essayer avec gcc -O -ftree-vrp)

                          blog.rom1v.com

                        • [^] # Re: Pas uniquement string

                          Posté par . Évalué à 1.

                          Non qu'on se comprenne bien.

                          Déreferencer un pointeur NULL va causer une exception.

                          mov [eax],0x1 avec eax=0x0000000000000000 va causer un access violation

                          Ensuite que le compilo optimise ta représentation en C et enlève les instructions qui déreferencent NULL, c'est une autre histoire.

                          • [^] # Re: Pas uniquement string

                            Posté par (page perso) . Évalué à 3. Dernière modification le 02/09/16 à 22:11.

                            +1

                            Par contre, vu qu'on parlait de l'initialisation des constructeurs en C++, on parlait bien de déréférencer "en C/C++", pas au niveau du binaire généré.

                            blog.rom1v.com

                      • [^] # Re: Pas uniquement string

                        Posté par (page perso) . Évalué à 4.

                        Bref tu peux penser ce que tu veux, mais si tu oses aller donner ton explication à n'importe quelle personne en sécurité (moi par exemple, qui a passé les 12 dernières années dans le milieu) tu vas leur filer un arrèt cardiaque tellement ton explication ne tient absolument pas la route et démontre un manque de compréhension des enjeux.

                        Je lis avec intérêt tes commentaires sur linuxfr, il en ressort que tu es compétant et surtout, tu as un avis différent de l'avis général, ce qui est souvent source de discussions intéressantes, mais qu'est ce que tu peux être impoli, c'est dingue ;)

                        Tu te permets de me traiter d’incompétent alors que tu n'as simplement pas compris mon discours, ou alors je me suis mal exprimé, on cherchera qui est le moins compétant en communication plus tard… Et tu te sers d'un argument d'autorité ce qui est petit.

                        Je ne veux absolument pas dire que le non initialisé est mieux ou moins bien que le initialisé par défaut, je veux dire que les deux sont dangereux. Mon argument est que les trucs par défaut arbitraire c'est dangereux. Si en tant que développeur tu veux le même comportement que celui par défaut, très bien, mais cela pourrait valoir le coup d'être explicite (çà c'est mon avis et c'est subjectif). Mais si tu ne veux pas le comportement par défaut mais qu'il apparaît parce que tu as fais une erreur et que tu n'est pas prévenu, et bien c'est faux et dangereux, et çà c'est factuel (enfin je pense, j'ai tort ?).

                        Oublions le cas des pointeurs qui est une particularité du fait de la valeur par défaut qui est nullptr et cela a plein d'implication marrantes que nous débattons. Mais qu'en est il d'autres types comme les entiers, les booléans où les char ? le \0 par défaut sur un char a certainement autant de chances de ne pas faire planter ton programme qu'un 42 ou un 12 par défaut, mais il est tout aussi faux si ce zéro n'était pas ton intention.

                        Sauf que le truc par défaut, le compilateur ne vas pas râler (son boulot c'est de mettre une valeur par défaut) et ton programme aura un résultat stable. Alors qu'une valeur non initialisé, le compilateur risque de râler et le programme n'aura pas un résultat stable, ce qui donne plus de chance de ce rendre compte du problème avant que le truc ne parte en production. J'ai eu un enseignant qui disant que le pire qui pouvait arriver dans un bug c'est que le programme ne plante pas, et le mieux c'est que cela plante, parce que cela permet de réaliser qu'il y a un bug. Et à ce niveau les valeur non initialisée augmentent les chances que cela plante comparée aux valeur par défaut arbitraire, d'où ma remarque initiale qui a lancée le "débat".

                        Bref, au final on est d'accord, non initialisé cela pue d'un point de vu sécurité, cela pue vraiment ! Mais par défaut, si ce par défaut n'est pas la valeur que tu veux, je trouve que cela pue aussi.

                        • [^] # Re: Pas uniquement string

                          Posté par . Évalué à 1.

                          Je lis avec intérêt tes commentaires sur linuxfr, il en ressort que tu es compétant et surtout, tu as un avis différent de l'avis général, ce qui est souvent source de discussions intéressantes, mais qu'est ce que tu peux être impoli, c'est dingue ;)

                          On va dire que des années dans la jungle de linuxfr m'ont endurci :)

                          Je ne veux absolument pas dire que le non initialisé est mieux ou moins bien que le initialisé par défaut, je veux dire que les deux sont dangereux.

                          Mai justement, moi ce que je te dis est que non-initialisé est PIRE que initialisé par défaut, et c'est factuel.

                          Ensuite cela ne veut pas dire que initialisé par défaut est super et idéal hein, mais à choisir entre les 2 un est bien plus sûr que l'autre.

                          Mais qu'en est il d'autres types comme les entiers, les booléans où les char ? le \0 par défaut sur un char a certainement autant de chances de ne pas faire planter ton programme qu'un 42 ou un 12 par défaut, mais il est tout aussi faux si ce zéro n'était pas ton intention.

                          Cela revient de nouveau au fait que la solution est meilleure que non-initialisé tout en n'étant pas idéale. Les valeurs par défaut choisies vont le plus souvent sauver le développeur qui n'initialise pas ses valeurs. Pas toujours, mais le plus souvent.

                          Idéalement le développeur choisit des options du compilo l'avertissant qu'il utilise une variable non initialisée. Mais sans cela, l'initialisation par défaut aide à sauver beaucoup de gens.

    • [^] # Re: Pas uniquement string

      Posté par (page perso) . Évalué à 1.

      Passionnant ! Merci !

  • # Duff's device

    Posté par . Évalué à 3.

    Tiens puisqu'on parle de switch, que pensez-vous de « Duff's device » ?

    Personnellement je trouve ça immonde et je ne vois que des cas rares où il faut vraiment produire le code plus performant possible au détriment de tout le reste qui peuvent rendre ce truc acceptable…

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Duff's device

      Posté par . Évalué à 2.

      je suis pour laisser le compilo gérer ce genre d'optimisation comme un grand; Ensuite ça illustre assez bien le fonctionnement d'un switch/case en C/C++.

      Il ne faut pas décorner les boeufs avant d'avoir semé le vent

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.