Partage
  • Partager sur Facebook
  • Partager sur Twitter

"Constructeur sur Constructeur"

Mais! Il devrait me dire qu'il y a un bug!

Sujet résolu
    23 août 2019 à 7:10:48

    Bonjour,

    J'aurai besoin d'une confirmation, car pour une fois, le compilateur ne me trouve pas de bug, alors que pour moi, il y en a un! (C'est bien la première fois, d'habitude c'est le contraire!)

    Lors de tests unitaires, d'une class qui a été modifié et re modifié, je me suis retrouvé dans le cas suivant (code grandement simplifié pour le forum):

    #include <iostream>
    
    class truc_t {
    public:
        truc_t (int const i) : m_truc{i}
        {}
    //private:
        int m_truc;
    };
    
    class doubleTruc_t {
    public:
        doubleTruc_t (truc_t const & t) : m_doubleTruc {t}
        {}
    //private:
        truc_t m_doubleTruc;
    };
    
    int main()
    {
        doubleTruc_t t{1};
        std::cout << t.m_doubleTruc.m_truc << std::endl;
        return 0;
    }
    

    Et là, non seulement ça compile, mais en plus, ça affiche "1" comme initialement (avant la création de la classe truc_t) !

    Hors je n'ai pas de constructeur de "doubleTruc_t" avec comme parametre int ! Mais c'est comme si, il avait automatiquement appelé le constructeur de "doubleTruc_t" avec l'objet résultant du constructeur de "truc_t" avec le parametre int 1.

    Alors, comme c'était merveilleux (pour une fois!), je l'ai fait avec un niveau de plus :

    #include <iostream>
    
    class truc_t {
    public:
        truc_t (int const i) : m_truc{i}
        {}
    //private:
        int m_truc;
    };
    
    class doubleTruc_t {
    public:
        doubleTruc_t (truc_t const & t) : m_doubleTruc {t}
        {}
    //private:
        truc_t m_doubleTruc;
    };
    
    class tripleTruc_t {
    public:
        tripleTruc_t (doubleTruc_t const & t) : m_tripleTruc {t}
        {}
    //private:
        doubleTruc_t m_tripleTruc;
    };
    
    int main()
    {
        tripleTruc_t t{1};
        std::cout << t.m_tripleTruc.m_doubleTruc.m_truc << std::endl;
        return 0;
    }

    Et là, le compilateur me jette avec une erreur incompréhensible (enfin quelque chose de normal!) :

    error: no matching function for call to ‘tripleTruc_t::tripleTruc_t(<brace-enclosed initializer list>)’

    A votre avis, c'est normal ?

    • Partager sur Facebook
    • Partager sur Twitter
      23 août 2019 à 8:59:14

      Oui c'est normal, en C++ une seule conversion est autorisée dans une chaîne de constructeur.

      Voir : https://en.cppreference.com/w/cpp/language/implicit_conversion

      Pour résoudre le problème, il suffit de mettre le type explicitement.

      class A {
      public:
      	A(int)
      	{
      	}
      };
      
      class B {
      private:
      	A a_;
      
      public:
      	B(const A& a)
      		: a_(a)
      	{
      	}
      };
      
      class C {
      private:
      	B b_;
      
      public:
      	C(const B& b)
      		: b_(b)
      	{
      	}
      };
      
      int main() {
      	B b(1);
      	// C c(123); no matching function for call to C::C(int)
      	C c{B{123}};
      
      	return 0;
      }

      Dans ton cas, je pense que ça fonctionnera si tu remplaces tripleTruc_t t{1} par tripleTruc_t t{doubleTruc_t{1}};

      -
      Edité par markand 23 août 2019 à 9:10:19

      • Partager sur Facebook
      • Partager sur Twitter

      git is great because Linus did it, mercurial is better because he didn't.

        23 août 2019 à 17:20:34

        Si on veut bloquer les conversions implicite au niveau du constructeur, il faut les marquer comme explicit.

        class truc_t {
        public:
            explicit truc_t (int const i) : m_truc{i}
            {}
        //private:
            int m_truc;
        };
        
        void foo(truc_t);
        
        foo(1); // erreur
        foo({1}); // erreur
        foo(truc_t{1}); // ok
        

        -
        Edité par jo_link_noir 23 août 2019 à 17:20:44

        • Partager sur Facebook
        • Partager sur Twitter
          23 août 2019 à 18:39:22

          Bonsoir et merci pour vos réponses.

          La bonne nouvelle c'est que c’est donc normal !

          La mauvaise, c'est que je ne comprends pas vos réponses: Les conversions entre short/int/long/unsigned int/float ... Ok, c'est classique. Mais là, il s'agit de constructeur: Je crée un "truc_t" a partir d'un argument, je ne le "change" pas sa représentation!o_O ....

          Ou alors, à l'inverse, je n'avais peut-être jamais compris les conversions: quand on fait une conversion de type (de int en float, par exemple), ça voudrait dire qu'on appel le constructeur du nouveau type (float, dans l'exemple) avec un argument d'un autre type (int dans l'exemple). C'est ça que vous voulez me dire ?

          Bien cordialement.

          • Partager sur Facebook
          • Partager sur Twitter
            23 août 2019 à 21:36:05

            Ton dernier message n'est pas super clair. Si je comprends bien, tu pensais que seuls les types primitifs pouvaient être convertis implicitement entre eux?

            Pre C++11, tout constructeur avec un seul argument et sans le mot clé "explicit" définit une conversion implicite du type de son argument vers le type de sa classe. Et à partir du C++11, c'est même le cas des constructeurs avec plusieurs arguments.

            https://en.cppreference.com/w/cpp/language/converting_constructor

            • Partager sur Facebook
            • Partager sur Twitter
              23 août 2019 à 23:18:01

              Salut,

              Dedeun a écrit:

              Bonsoir et merci pour vos réponses.

              La bonne nouvelle c'est que c’est donc normal !

              La mauvaise, c'est que je ne comprends pas vos réponses: Les conversions entre short/int/long/unsigned int/float ... Ok, c'est classique. Mais là, il s'agit de constructeur: Je crée un "truc_t" a partir d'un argument, je ne le "change" pas sa représentation!o_O ....

              Oui, il faut dire que ce n'est pas forcément des plus intuitifs :p

              Car nous sommes bien d'accord que le rapport entre un int et une classe qui prendrait la forme de

              class Truc {
              public:
                 Truc (int i);
              };

              est loin d'être évident, n'est-ce pas ? Et, pourtant, il y en a un, à cause du constructeur qui prend un entier comme paramètre va -- justement -- agir exactement à la manière un opérateur de conversion implicite.

              C'est assez surprenant, mais on peut tout à fait écrire un code proche de

              int main(){
                  Truc a = 3;
                  std::cout<<a.i<<"\n";
              }

              qui affichera 3

              Et pour bien te montrer que ce n'est pas du au hasard, on peut faire la même chose en disposant d'une variable de type int:

              int main(){
                  int i = 10;
                  Truc b = i;
                  std::cout<<b.i<<"\n";
              }

              qui affichera 10 cette fois ;)

              C'est exactement le même principe que celui qui te permet de convertir un short en int:

              int main(){
                 short s = 3;
                 int i = s; // sa marche!
              }

              Le ... truc, c'est que C++ met quand même une limite au nombre de conversions implicites qui peuvent être effectuées: on n'a droit qu'à UNE et UNE SEULE conversion implicite, ni plus, ni moins :

              class Truc{
              public:
                  Truc(int i):i{i}{}
                  int i;
              };
              class Brol{
              public:
                  Brol(Truc const & t):t{t}{
                      
                  }
                  Truc t;
              };
              int main(){
                  Truc t = 1; // une conversion
                  Brol b = t; // une conversion
                  /* MAIS MAIS MAIS
                  Brol b2 = 3; sera refusé : deux conversions seraient 
                               nécessaires ( int->Truc->Brol )
                  */
              }

              Alors, bien sur, le gros problème avec les "comportements par défaut", c'est que l'on va toujours trouver des situations dans lesquelles... on aurait préféré qu'ils agissent autrement :'(

              Et donc, si tu ne veux pas que la conversion puisse se faire automatique, et il y a pas mal de raisons qui pourraient plaider en ce sens, tu peux toujours déclarer ton constructeur comme explicit :

              class NoConversion{
              public:
                  explicit NoConversion(int i):i{i}{
                  }
                  int i;
              };
              int main(){
                  /* autorisé : on fait appel au constructeur directement */
                  NoConversion nc{1};
                  /* MAIS MAIS MAIS */
                  NoConversion erreur = 1; //sera refusé : conversion impossible
              }

              Est-ce que cela te parait un peu plus clair, maintenant?

              • Partager sur Facebook
              • Partager sur Twitter
              Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                24 août 2019 à 8:57:49

                Bonjour, et merci encore pour vos réponses.

                SpaceIn a écrit:

                Ton dernier message n'est pas super clair. Si je comprends bien, tu pensais que seuls les types primitifs pouvaient être convertis implicitement entre eux?

                 Oui, je viens du monde du C.

                Merci SpaceIn pour l'explication et le raccourcie, et Merci au long long message de Koala01, (qui lui de plus est en français  :) ) C'est maintenant clair! On peut pas faire mieux!

                Juste encore une question, (et si j'ai bien compris, vous allez me répondre oui!):
                Quand on fait un cast (c'est à dire une conversion explicite), c'est pareil en fait, on appel le constucteur du type destination ?

                Bien cordialement, et bon weekend.

                -
                Edité par Dedeun 24 août 2019 à 8:59:19

                • Partager sur Facebook
                • Partager sur Twitter
                  24 août 2019 à 12:06:46

                  Dedeun a écrit:

                  Quand on fait un cast (c'est à dire une conversion explicite), c'est pareil en fait, on appel le constucteur du type destination ?

                  Non...

                  Pour un cast explicite "à la C" (du genre de (autretype) maVariable), on indique simplement au compilateur qu'il doit considérer maVariable (qui est d'un type donné) est en réalité d'un autre type. En gros, on ment au compilateur.

                  En C++, les choses vont encore plus loin, car on déconseille le cast "à la C", pour une raison toute bête : on dispose de quatre cast différents:

                  reinterpret_cast : qui agit exactement comme le cast du C:

                  char word[4];
                  reinterpret_cast<int *>(word)=33444666; // on demande au compilateur
                                                          // de considérer que
                                                          // word est un int

                  le static_cast : qui demande au compilateur de vérifier si la donnée est d'un type compatible avec le type que l'on veut prétendre qu'elle est

                  le dynamic_cast : qui demande au compilateur de placer une vérification à l'exécution du type de la variable, par exemple

                  class Base{
                      /* ... */
                  };
                  class Derivee : public Base{
                      void specific();
                  };
                  class Autre : public Base{
                  };
                  int main(){
                      Base * b= new Derivee;
                      /* le cast renverra nullptr si b n'est pas de type
                       * Derivee
                       */
                      Derivee cast =dynamic_cast<Derivee*>(b);  
                      if(cast)
                          cast->specific();
                  }

                  et le const_cast : qui permet de changer la constance d'une donnée (non const --> const  ou const --> non const). Note que l'on peut toujours "rajouter la constance si elle n'est pas présente:

                  int main(){
                      int i=15; // non constant
                      int const & ref = i; // constant, aucun problème
                  }

                  mais que l'on ne peut pas retirer de la constance si elle est présente

                  int main(){
                      int const i = 15;
                      int & ref = i; // refusé... l'alternative
                      const_cast<int &>(i) = 32; // accepté, mais très dangereux
                  }
                  • Partager sur Facebook
                  • Partager sur Twitter
                  Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                    24 août 2019 à 13:16:06

                    Ce que dit koala01 sur les cast est vrai, mais uniquement pour les références et pointeur. Dans le cas d'un type plein (c.-à-d. hors référence et pointeur), c'est le constructeur qui est utilisé.

                    La raison est simple, les casts avec pointeur/référence ne modifie pas l'objet, juste la représentation du type utilisé par le compilateur. Les 2 pointeurs font référence à la même chose et modifier la valeur pointée de l'un modifie forcément le contenu des 2 pointés puisqu'ils sont identiques. Dans le cas d'un type plein, cela fait une conversion et donc des appels aux constructeurs ou opérateurs de cast.

                    • Partager sur Facebook
                    • Partager sur Twitter
                      24 août 2019 à 14:29:54

                      A nouveau merci pour ces réponses.

                      En effet Koala01, je n'étais pas assez précis quand je parlais de cast. En fait je parlais de static_cast (comme j'ai mis tous les warning dans les options de compilation, il est courant que j'ai des warning sur des modifications de type en particulier de int --> char, ou autre. Aussi j'utilise le static_cast pour les supprimer !).

                      Je ne m'étais pas encore posé la question du "cast" des références ni des pointeurs. Cool d'avoir ouvert le sujet! et mon intérêt :)

                      Et pour les "types pleins", Jo_link_noir, tu confirmes ce que je supposais. (Je n'ai pas encore le réflexe de chercher sur cppreference.com, ... il faudra à l'avenir! ... Mais c'est un peu compliqué de faire le lien entre son cas pratique et un mot clef dans cppreference.com !)

                      En tous cas merci d'avoir pris le temps de répondre. ("Je serait moins bête ce soir!").

                      • Partager sur Facebook
                      • Partager sur Twitter
                        26 août 2019 à 7:00:19

                        Je me permets de ré ouvrir ce post, car en poursuivant dans le même sens, je suis tombé sur ce qui suit! (encore un truc bizard!)

                        #include <string>
                        
                        class truc_t {
                        public:
                            truc_t (std::string const& str) : m_str{str}
                            {}
                        private:
                            std::string m_str;
                        };
                        
                        class doubleTruc_t {
                        public:
                            doubleTruc_t (truc_t const& truc) : m_truc{truc}
                            {}
                        private:
                            truc_t m_truc;
                        };
                        
                        int main()
                        {
                            truc_t t0 {""};
                        //    doubleTruc_t tt0{""};
                            return 0;
                        }
                        

                        Hors si je ne me trompe pas "", c'est un litéral de type char *; donc quand je fais un truc_t {""}, je fais 2 conversions implicites de suite: char * --> std::string et std::string --> truc_t. ("Isn't it ?" comme ils disent chez les mangeurs de sauce à la menthe!)

                        Peut-être qu'il y a encore des subtilités avec un littéral chaîne de caractères.

                        (PS et là, cppreference.com ne m'a pas donné la réponse ... )

                        -
                        Edité par Dedeun 26 août 2019 à 7:02:50

                        • Partager sur Facebook
                        • Partager sur Twitter
                          26 août 2019 à 12:03:35

                          Tu te trompes: en écrivant truc_t t0{""}, tu fais explicitement appel au constructeur de la classe, si bien que tu as

                          • une conversion char *  ->std::string
                          • un appel au constructeur

                          Ce qui est tout bon :D

                          Par contre, truc_t t1= ""; aurait été refusé, car tu aurais effectivement eu deux conversions en suivant : char * ->std::string -> truc_t.

                          Notes d'ailleurs que, depuisC++14 (c'est moins vieuw que ce que je ne le croyais), on peut utiliser l'opérateur s qui se trouve dans l'espace de noms std::literals, et qui nous permet de créer directement une std::string. ce qui fait qu'un code proche de

                          int main(){
                              using namespace std::litterals;
                              /* divisons pour mieux régner: */
                              auto str = ""s;
                              truc_t t2= str;
                          }

                          serait parfaitement été accepté, car str serait de type std::string (il aurait aussi été accepté sous la forme de truc_t t2 = ""s;, pour la même raison ;) ), et il n'y aurait du coup eu qu'une seule conversion ;) .

                          Et, du coup, le code

                          int main(){
                              using namespace std::literals;
                              double_truc_t dt{""s};
                          }

                          serait lui aussi accepté, toujours pour les même raisons : tu as une (et une seule) conversion de std::string -> truc_t, et un appel explicite au constructeur de double_truc_t ;)

                          -
                          Edité par koala01 26 août 2019 à 12:13:10

                          • Partager sur Facebook
                          • Partager sur Twitter
                          Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                            26 août 2019 à 17:33:54

                            Je vais juste chipoter sur un détail :D

                            > "" est un littéral de type char *

                            En fait non, c'est un tableau constant de caractère qui contient un zéro terminal: const char [1]. "abc" -> const char[4]{'a','b','c','\0'}. Mais lorsqu'on le passe en paramètre de fonction, il devient silencieusement un char const*.

                            • Partager sur Facebook
                            • Partager sur Twitter
                              26 août 2019 à 17:54:16

                              Ok, Ca y est ... Eurêka! J'ai compris :magicien:  ... (Enfin, jusqu'à la prochaine fois !  >_< il faut pas croire que j'ai tous compris ...)

                              Donc on peut appeler un constructeur qui lui fait une conversion implicite ... Ok!

                              <---------------------------->

                              Edit: J'avais pas vu la réponse de Jo_Link_Noir .... Ok, c'est vrais, mais ... bon, quand on le passe en argument il devient un char * (enfin, sauf si c'est différent du C ...)

                              -
                              Edité par Dedeun 26 août 2019 à 19:13:32

                              • Partager sur Facebook
                              • Partager sur Twitter
                                26 août 2019 à 18:15:08

                                Dedeun a écrit:

                                Ok, Ca y est ... Eurêka! J'ai compris :magicien:  ... (Enfin, jusqu'à la prochaine fois !  >_< il faut pas croire que j'ai tous compris ...)

                                Donc on peut appeler un constructeur qui lui fait une conversion implicite ... Ok!

                                Voilà, c'est aussi simple que cela :D

                                • Partager sur Facebook
                                • Partager sur Twitter
                                Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait

                                "Constructeur sur Constructeur"

                                × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                                × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                                • Editeur
                                • Markdown