Partage
  • Partager sur Facebook
  • Partager sur Twitter

Retour non-const dans fonction const

Sujet résolu
    1 juin 2019 à 9:27:50

    Bonjour,

    Voici un code qui ne marche pas (peut-être même indéterminé qui sait ?) :

    vector<Point>::iterator Polyline::closest_node(Point const& pt) const
    {
        vector<Point>::const_iterator closest;
        double dist{ numeric_limits<double>::infinity() };
        for(vector<Point>::const_iterator it{cbegin()}; it != cend(); ++it)
        {
            if( dist > it->plani().distance(pt.plani()) )
            {
                closest = it;
                dist = closest->plani().distance(pt.plani());
            }
        }
        return const_cast<vector<Point>::iterator>(closest);
    }

    La fonction elle-même n'a pas vocation à modifier la polyligne. Je l'ai donc logiquement marquée const et tous les itérateurs à l’intérieur sont const.

    En revanche, ce que l'utilisateur fait ensuite de l'itérateur retourné ne concerne en rien ladite fonction. Je voudrais donc récupérer un iterator et non un const_iterator.

    Est-ce possible de faire cela proprement ? Actuellement j'ai résolu le problème en utilisant une fonction non constante. Mais ça me parait moins rigoureux.

    -
    Edité par Megalo 1 juin 2019 à 9:28:18

    • Partager sur Facebook
    • Partager sur Twitter
      1 juin 2019 à 10:21:33

      Salut,

      Megalo a écrit:

      Bonjour,

      La fonction elle-même n'a pas vocation à modifier la polyligne. Je l'ai donc logiquement marquée const et tous les itérateurs à l’intérieur sont const.

      Faux!!!!

      Si l'utilisateur de la fonction veux modifier l'itérateur, tu modifies le contenu du tableau et, par conséquent, tu modifies forcément la classe qui le contient ;)

      Si la valeur renvoyée par une fonction accepte des modifications et, surtout, répercute les modifications apportée sur la donnée originelle de la classe, un code proche de

      poly.closest_node(aPoint)->setX(23);

      revient exactement au même que si la fonction avait pris la forme de

      void Polyline::move_closest(Point const & p, int x, int y){
          /* on cherche l'itérateur qui va bien */
          it->setX(x);
          it->setY(y);
      }

      (D'ailleurs, je te conseillerais volontiers de créer une telle fonction ;) )

      Megalo a écrit:

      La fonction elle-même n'a pas vocation à modifier la polyligne. Je l'ai donc logiquement marquée const et tous les itérateurs à l’intérieur sont const.

      En revanche, ce que l'utilisateur fait ensuite de l'itérateur retourné ne concerne en rien ladite fonction. Je voudrais donc récupérer un iterator et non un const_iterator.

      Est-ce possible de faire cela proprement ? Actuellement j'ai résolu le problème en utilisant une fonction non constante. Mais ça me parait moins rigoureux.

      Crées peut-être une version constante, renvoyant un const_iterator pour quand l'utilisateur veut juste obtenir l'itérateur (et l'utiliser en "lecture seule") et une autre version non const, renvoyant un itéateur, pour quand l'utilisateur veux modifier l'élément renvoyé

      Par contre, j'ai l'impression (mais je peux me tromper) que ta classe Polyline hériterait peut-être bien de std::vector...  C'est une très mauvaise idée, car les collections de la STL ne sont pas destinées à être dérivées ;)

      Une simple agrégation serait sûrement bien plus adaptée

      -
      Edité par koala01 1 juin 2019 à 10:29:09

      • 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
        1 juin 2019 à 12:44:21

        Merci de ta réponse.

        > Crées peut-être une version constante, renvoyant un const_iterator pour quand l'utilisateur veut juste obtenir l'itérateur (et l'utiliser en "lecture seule") et une autre version non const, renvoyant un itéateur, pour quand l'utilisateur veux modifier l'élément renvoyé

        En fait c'est la solution que j'ai adoptée jusqu'à présent.

        > Par contre, j'ai l'impression (mais je peux me tromper) que ta classe Polyline hériterait peut-être bien de std::vector... C'est une très mauvaise idée, car les collections de la STL ne sont pas destinées à être dérivées ;)

        J'y ai effectivement pensé. Mais après quelque recherche dont les résultats vont dans ton sens, j'ai simplement créer un attribut vector<point> et réécris la plupart des méthodes, front(), begin(), operator[], etc. pour pouvoir utiliser ma polyligne comme un conteneur. Si tu connais moins verbeux, je suis preneur.

        > Faux!!!! > Si l'utilisateur de la fonction veux modifier l'itérateur, tu modifies le contenu du tableau et, par conséquent, tu modifies forcément la classe qui le contient ;)

        C'est là que j'ai du mal à te suivre. Si je fais :

        vector&lt;point&gt;::iterator it{ ma_polyligne.closest_node(un_point)};
        it-&gt;set_attribut(nouvelle_valeur);
        

        ma fonction closest_node ne modifiera JAMAIS l'objet, non ?

        PS : Je n'arrive pas à citer un morceaux de post, par exemple en sélectionnant le texte avant de cliquer sur « citer ». Je n'ai pas de bouton « citation » à côté de « code », « émoji », et autre options de formatage, le markdown n'as pas l'air de mieux fonctionner. Faux vraiment utiliser du html pour formater son texte, ou ya un truc que je fais mal ?</point>

        -
        Edité par Megalo 1 juin 2019 à 12:49:28

        • Partager sur Facebook
        • Partager sur Twitter
          1 juin 2019 à 12:50:47

          Tu ne peux pas te permettre de faire un const_cast sur l'itérateur, cela ne fonctionnera pas partout. Mais c'est vrai que faire 2 fonctions, une constante, une autre non const est pénible. (Il faudrait un auto(const) xD)

          Ce qui est simple à faire par contre avec des itérateurs de std::vector, est une fonction qui convertit un itérateur const en non const: vec.begin() + (it - vec.cbegin()). Et si ensuite tu veux les 2 versions de closest_node, il suffira dans la version non const de faire return to_mutable_iterator(closest_node(pt)).

          Quand cela est plus complexe, je passe généralement par une fonction intermédiaire templaté sur le conteneur.

          • Partager sur Facebook
          • Partager sur Twitter
            1 juin 2019 à 12:52:38

            Ça marche.

            Merci de vos réponse. Dans l'absolut la version avec une surcharge non const fera l'affaire et sera plus simple que les autres solutions proposés.

            Mais c'est vrai que la logique m'échappait un peu. Je trouve ça bizarre qu'une fonction qui retourne un pointeur non-const doivent elle-même être forcément non const. Peut-être une logique d'encapsulation ? Je vais essayer de méditer là-dessus.



            -
            Edité par Megalo 1 juin 2019 à 13:01:05

            • Partager sur Facebook
            • Partager sur Twitter
              1 juin 2019 à 16:20:47

              Megalo a écrit:


              > Faux!!!! > Si l'utilisateur de la fonction veux modifier l'itérateur, tu modifies le contenu du tableau et, par conséquent, tu modifies forcément la classe qui le contient ;)

              C'est là que j'ai du mal à te suivre. Si je fais :

              vector&lt;point&gt;::iterator it{ ma_polyligne.closest_node(un_point)}; it-&gt;set_attribut(nouvelle_valeur); 

              ma fonction closest_node ne modifiera JAMAIS l'objet, non ?


              HéHé... il est particulièrement rare que l'on puisse se contenter de ne prendre que l'appel d'une fonction en ligne de compte.  Surtout si la fonction est sensée renvoyer "quelque chose".

              Il est donc souvent utile d'étendre sa perception jusqu'à la notion d'expression exécutée (pour faire simple : tout ce qui se trouve entre deux point-virgule successifs), juste pour voir ce qui en sortira.

              On se rend alors compte qu'il y a des expressions qui ne provoquent effectivement aucune modification de l'objet d'origine, par exemple

              Point p = *(poly.closest_node(a_point));

              qui nous permet de travailler sur p  sans le moindre problème, vu que p n'est qu'une copie du point référencé par l'itérateur.

              Mais imagine un instant que l'utilisateur de ta classe écrive une expression proche de

              poly.closest_node(un_point)->setX(25);

              qui, pour être précis, pourrait tout aussi bien être subdivisé en deux expressions distinctes telles que

              Point & ref= *(poly.closest_node(un_point));
              ref.setX(25);

              L'appel à la fonction setX, il se fait sur quel élément à ton avis?

              Hé bien d'après le prototype (et le nom ) de ta fonction, il se fait sur (l'élément représenté par)... l'itérateur qui correspond au point le plus proche de celui que tu as donné comme point de référence.  Nous sommes d'accord, jusqu'ici?

              Mais, cela veut dire que, si tu avais un polyline dont les différents points étaient

              3,2
              5,7
              9,11 -->considérons que voici le point correspondant à l'itérateur renvoyé
              13,15

              Tu te retrouverais, après l'appel à setX, avec un polyline dont les différents points sont

              3,2
              5,7
              25,11 -->considérons que voici le point correspondant à l'itérateur renvoyé
              13,15

              Je présumes que nous sommes toujours d'accord sur ce point, non?  Et si c'est le cas, tu ne peux pas nier que les points correspondant au polyline résultant sont différents de ceux du polyline d'origine.  Sommes-nous toujours bons jusque là?

              Et comme c'est l'itérateur qui a été renvoyé par la fonction (closest_node) qui a permis cette modification, tu n'a pas vraiment d'autre choix que d'admettre que... la fonction closest_node a provoqué (fusse de manière indirecte) la modification de l'instance.

              Maintenant, je comprends ta frustation, car on passe notre temps à répéter qu'il n'y a pas de demi mesure, que le courant passe, ou qu'il ne passe pas, et qu'il faut donc appliquer les règles de manière particulièrement rigoureuses.

              Or, comme tu l'as si bien appris, on dit qu'

              une fonction membre constante est une fonction qui ne modifie en aucun cas l'état de l'objet à partir duquel elle est invoquée

              Pour être tout à fait honnête, je crois que le terme que tu semble oublier est "en aucun cas", mais bon, peut-être est-il temps de peaufiner un tout petit peu cette règle pour que tu puisse l'appliquer à bon escient :D.

              Car, ce terme "en aucun cas" ne veut pas seulement dire que, quoi que puisse faire la fonction, elle ne devra pas occasionner une modification de l'état de l'objet.

              Bien sur, cela signifie aussi cela, mais, bien au delà, cela signifie (aussi ? / surtout ? )que, si la fonction renvoit une donnée  à la fonction appelante, quelque soit la modification que nous pourrions envisager d'apporter à cette donnée, elle ne pourra pas être répercutée sur l'objet d'origine.

              L'exemple qui fait appel à setX ne respecte pas la règle imposée lorsque l'on parle de fonctions membres constantes ;)

              • 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
                9 juin 2019 à 11:41:28

                Avec le recul, je crois qu'il faut le prendre comme une simple règle inhérente à C++. C'est un détail qui mériterait d'être mieux développé dans les différents tutoriels je pense.

                Merci de tes réponses en tout cas.

                -
                Edité par Megalo 9 juin 2019 à 12:09:03

                • Partager sur Facebook
                • Partager sur Twitter
                  9 juin 2019 à 22:08:17

                  En fait, le principe est simple, même si je n'ai encore trouvé aucun terme adapté pour le traduire: c'est le principe de la const-correctness.

                  Ce principe se base sur une règle simple :

                  une donnée déclarée constante ne peut en aucun cas être modifiée (*)

                   Merci Mr Obvious :D

                  C'est bien beau, cette règle, mais le fait est que les fonctions membres (et les fonctions déclarées amies) ont un accès illimité aux "données internes" qui composent la classe et que, par nature, une fonction membre permet donc "forcément" de modifier ces "données internes".

                  Pour pouvoir séparer "le bon grain (les fonctions membres qui ne risquent pas de modifier la donnée constante) de l'ivraie (les fonctions qui risquent de modifier la donnée)", la règle est de dire explicitement que la fonction s'engage à ne modifier en aucun cas la donnée à partir de laquelle elle est appelée.  Comment? en déclarant explicitement cette fonction comme constante, bien sur.

                  Et cela nous mène à édicter une deuxième règle:

                  Toutes les données internes (**) qui composent l'instance d'une classe à partir de laquelle on fait appel à une fonction constante sont obligatoirement considérées comme étant constantes, ad minima, pour la durée d'exécution de la fonction

                  Ce qui est "logique", parce que si la fonction membre constante venait à modifier l'une des données internes, cela provoquerait forcément une modification de l'instance de la classe à partir de laquelle elle a été appelée.  Or, cette instance est... sensée être constante (et donc, ne peut normalement pas être modifiée).

                  Mais, le fait de parler de fonction (membre, en l'occurrence) nous pose un autre problème:

                  Une fonction peut renvoyer (ou non) une donnée.  Et comme une fonction membre a accès à l'ensemble des données internes de la classe, elle peut donc renvoyer ... la valeur d'une des données internes de l'instance à partir de laquelle elle a été appelée.

                  Or, si une fonction membre décide de renvoyer une donnée interne, il y a deux solutions:

                  • Soit on renvoie une copie de la donnée en question (ce qui est le cas par défaut ... ex : int MaClasse::getX() const)
                  • Soit, pour une raison qui n'appartient qu'à nous, on souhaite éviter la copie de la donnée renvoyée et nous décidons alors de la renvoyer ... sous forme de référence (ou sous forme de pointeur).

                  Oui, mais!  Si l'on renvoie la donnée membre sous forme de copie, il n'y aura aucun problème: la modification de la copie ne modifiera pas la donnée d'origine, ce qui respecte parfaitement la première et la deuxième règle.

                  Par contre, si on décide de renvoyer -- pour une raison qui ne tient qu'à nous -- la donnée membre sous la forme d'un pointeur ou d'une référence, toutes les modifications que nous pourrions apporter à la donnée référencée seraient... appliquées à la donnée membre:colere:.  Ce qui serait totalement contraire à la première règle que j'ai citée, vu que de telles modifications provoqueraient -- de facto -- une modification de la donnée qui est sensée ... ne pas être modifiée :'(:waw:.

                  Et c'est pour cela que la troisième règle existe:

                  Si l'on peut décider à n'importe quel moment de rajouter un état de constance à  une donnée qui n'est pas constante, on ne peut en aucun cas (*) décider de retirer l'état de constance à  une donnée qui est constante.

                  Tu remarqueras que cette règle présente là encore une certaine logique, car le fait de considérer "temporairement" comme constante une donnée qui ne l'est pas ne fait ... de rajouter des restrictions quant à l'usage que l'on peut en faire, alors que, si on décide de lever -- serait-ce que temporairement -- la constance d'une donnée qui est constante, on s'offre la possibilité d'apporter des modifications ... qui n'auraient jamais du être admises pour une donnée qui ... ne pouvait normalement pas être modifiée.

                  Et c'est la raison pour laquelle le compilateur acceptera d'une fonction membre constante qu'elle renvoie la valeur d'une donnée membre soit par valeur / copie, soit sous forme de référence constante, mais qu'il refusera qu'elle soit renvoyée sous forme de pointeur ou de référence non constant(e).

                  Une fois que tu as compris ces trois règles, le reste "coule de source" ;)

                  (* à moins, bien sur, de passer par un const_cast)

                  (** à l'exception des données déclarées comme étant mutable)

                  • 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
                    14 juin 2019 à 12:37:34

                    Ça a effectivement sa logique, je le reconnais.

                    Mais c’est assez surprenant, et assez peu expliqué dans les quelques tutoriel que j’ai pus lire. Je prends la règle que tu cites :

                    « Toutes les données internes (**) qui composent l'instance d'une classe à partir de laquelle on fait appel à une fonction constante sont obligatoirement considérées comme étant constantes, ad minima, pour la durée d'exécution de la fonction »

                    En fait, le « ad minima, pour la durée d’exécution de la fonction » est superflu, puisqu’elle garde le caractère constant bien au-delà et quoiqu’il arrive.

                    • Partager sur Facebook
                    • Partager sur Twitter

                    Retour non-const dans fonction const

                    × 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