Partage
  • Partager sur Facebook
  • Partager sur Twitter

Définir correctement les classes

Pb dans la conceptualisation sur une application de maillage

    10 septembre 2020 à 13:57:25

    Nouveau en C++, je veux m'exercer sur la base d'un cas concret qui m'intéresse : construction d'un maillage triangulaire dans le plan sur la base de noeuds imposés, et interpolation dans ce maillage.

    Concrètement, un maillage triangulaire ressemble à cela :

    L'algorithme général est clair dans ma tête, mais la phase de conceptualisation en C++ me pose quelques difficultés, notamment comment définir correctement mes classes.

    Quelle que soit ma construction des classes, les entités à manipuler sont les suivantes :

    1) Les noeuds :

    - Ensemble de n points fixés : abscisse, ordonnée, valeur de la fonction à cet endroit

    - L'algorithme impose un accès direct aux coordonnées de tout noeud => les ranger dans un tableau plutôt que dans une liste

    - L'ensemble est fixe, mais il faut quand même le constituer au départ : lecture des données sur un fichier (il peut y avoir des centaines de points) et chargement du tableau.

    - 3 conteneurs de type vector < double > me semblent appropriés

    2) La maille :

    - Une maille est un triangle constitué par 3 points parmi les noeuds

    - Un noeud peut être commun à plusieurs mailles

    - Il semble logique de définir la maille par les trois indices des points qui la constituent => 3 entiers

    - La maille peut constituer un objet en soi, car pas mal de méthodes relèvent du "niveau maille" : calcul de l'air du triangle, du cercle circonscrit, statuer si un point donné par l'utilisateur est dans le triangle ou en dehors, ...

    D'où :

    class Maille
    { 
    private :
    int m_i0;  // Indice du 1er sommet dans le tableau de noeuds
    int m_i1;  // Indice du 2eme sommet dans le tableau de noeuds
    int m_i2;  // Indice du 3eme sommet dans le tableau de noeuds
    etc...
    };

    3) Le maillage :

    - C'est l'ensemble des mailles.

    - Contrairement aux noeuds, l'accès direct à une maille donnée ne présente pas d'intérêt : pour statuer dans quelle maille se situe un point donné par l'utilisateur, il faut balayer toutes les mailles jusqu'à temps trouver la bonne.

    - Utiliser une liste de mailles plutôt qu'un tableau de mailles fait l'affaire, et n'exige pas la contiguité de l'espace mémoire.

    - Le maillage peut constituer un objet, car pas mal de méthodes relèvent du "niveau maillage" : constitution incrémentale du maillage, vérification de la qualité du maillage, ... et c'est de toute façon l'entité à laquelle s'intéresse l'utilisateur.

    - D'où l'idée de constituer une classe maillage, avec pour attribut essentiel la liste des mailles (il y en a d'autres dont je ne parle pas ici)

    class Maillage
    {
    private :
    list <Maille> m_mesh;
    etc...
    };


    Du coup, où mettre les noeuds ?

    Solution 1 : comme les noeuds dictent le maillage (modifier un noeud implique de refaire le maillage), il serait logique d'inclure les 3 vectors des noeuds comme attributs (constitutifs) du Maillage.

    Mais le hic est que les méthodes de la classe Maille doivent accéder aux coordonnées des noeuds (donc à ces vectors), et ce, des dizaines de milliers de fois. Procéder par accesseur risque d'être catastrophique au niveau des temps d'exécution.

    Solution 2 : déclarer ces 3 vectors publics. Mais "ça fait sale" => pas très POO... sachant qu’ils seraient accessibles (et modifiables) par l'utilisateur (niveau main) ! Et ça, je n’en veux pas.

    Solution 3 : Solution 1 en déclarant la classe Maille amie de la classe Maillage.

    Ca résoud mon problème, mais la littérature dit que l'usage de "friend" révèle souvent un défaut de conception des classes. Dans mon cas, j'ai du mal à voir lequel.

    Qu'est-ce que j'ai raté ? Quelqu'un aurait-il une meilleure idée de définition de classes ?

    • Partager sur Facebook
    • Partager sur Twitter
      10 septembre 2020 à 14:47:00

      Lu',

      Je pense que ta définition de classes est plutôt correcte et correspond à peu près à ce que font les fichiers gerant les models 3D. Ton problème est que tu essaies d'optimiser un truc qui n'existe même pas encore.

      tbc11 a écrit:

      Mais le hic est que les méthodes de la classe Maille doivent accéder aux coordonnées des noeuds (donc à ces vectors), et ce, des dizaines de milliers de fois. Procéder par accesseur risque d'être catastrophique au niveau des temps d'exécution.

      C'est vrai en théorie, mais en pratique tu n'en sauras rien tant que tu ne l'auras pas testé, à cause (grâce à) des optimisations faites par le compilateur.

      Pour faire simple: Essaie d'abord d'avoir un truc qui fonctionne et seulement ensuite tu essaieras de l'optimiser.

      Comme disait l'autre : "Premature optimization is the root of all evil"



      • Partager sur Facebook
      • Partager sur Twitter

      Eug

        10 septembre 2020 à 15:21:11

        Ne te force pas à faire de l'objet pour le principe d'en faire. Il faut que cela soit naturel. Si dans un premier temps tu n'as qu'un truc à prendre du C++: c'est la gestion transparente de la mémoire.

        L'objet va apporter plusieurs choses:

        - tu as avoir des entités qui vont faire des choses pour toi. Et quand je parle de faire, c'est vraiment de réaliser des transformations, et pas juste de stocker des données et donner un accès à ces dernières. C'est ~ l'abstraction.

        - réaliser ces transformations de manière cohérente, probablement en cachant des rouages internes, mais surtout de façon à garantir des invariants. Si tes données ne sont que des agrégats sans invariants, alors elles n'ont aucune raison de devenir des objets -- sauf pour donner quelques services, comme p.ex. une algèbre sur points et vecteurs. On rejoint l'encapsulation.

        - une adaptation  dynamique des comportements: comme les if/elif/... mais plus proches des tables de pointeurs de fonctions, sauf que c'est sans huile de coude et avec des bonnes perfs. C'est le polymorphisme.

        Bref, utilise des objets si cela répond à des besoins. Employer des fonctions libres n'est pas le mal incarné, bien au contraire.

        ----

        Sinon, on réserve les listes quand on n'a pas le choix pour des propriétés de non invalidation, ce genre de choses -- i.e. les maillons autour d'un maillon retiré restent valables après retrait. Les perfs sont mauvaises à cause des fautes de cache.

        Quand on fait du numérique intensif et que l'on veut des perfs, ce qui marche, ce sont les structs of Arrays (SoA), et l'objet, c'en est l'antithèse. Et du coup cela peut avoir des conséquences profondes sur le design. Visiblement, tu sembles déjà parti dans cette direction.

        ----

        L'amitié sert à renforcer l'encapsulation. En effet les alternatives c'est de tout mettre public, ou passer par des setters qui n'encapsulent donc rien. Les amis, cela a les mêmes droits que les membres, c'est en même nombre fini, c'est autant contrôlé par nous, sauf qu'au lieu d’être dedans, c'est dehors.

        -----

        Un accesseur inline ne va rien coûter en perfs. L'assembleur sera le même.

        • Partager sur Facebook
        • Partager sur Twitter
        C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
          10 septembre 2020 à 17:08:01

          Salut,

          tbc11 a écrit:

          - Ensemble de n points fixés : abscisse, ordonnée, valeur de la fonction à cet endroit

          Commencons déjà par nous intéresser à cela, si tu veux bien:

          Car tu viens de parler de "points".  Cette notion existe-t-elle? es tu capable de l'utiliser?

          au pire, tu peux la créer, car nous en aurons forcément besoin, sous une forme proche de

          struct Point{
              double x; // l'abscisse
              double y; // l'ordonée
              /* tu nous as dit que tout était dans le même plan, 
               * nous n'avons donc pas besoin de la troisième coordonnée
               */
          };

          Il me semble que tu as parlé de noeud, qui est une notion  représentant l'association d'une valeur et d'un point.  Cette notion existe-t-elle? es-tu capable de la manipuler?

          encore une fois, il est tout à fait possible de la créer, car le fait est que nous en aurons besoin.  Elle pourrait prendre une forme proche de

          struct Node{
             Point position;
             /* tu ne dis rien sur les valeurs qui seront utilisées.
              * pour la simplicité, je vais considérer que ce sont
              * simplement des valeurs entières
              */
             double value;
          };

          Notes ici que je fais volontairement très simple: je pourrais représenter cette notion sous une forme générique, acceptant n'importe quel type de valeur, en lui donnant une forme (autorisée par C++, je ne sais pas trop si tu as le niveau pour comprendre ou non) proche de

          template <typename T>
          struct Node{
              /* parce que j'aime bien être en mesure de connaitre 
               * le type de la donnée que je manipule
               */
              using value_type = T;
              Point posiiton;
              value_type value;
          };

          tbc11 a écrit:

          - 3 conteneurs de type vector < double > me semblent appropriés

          Heuu... pour représenter quoi exactement?

          Si c'est pour représenter respectivement les coordonnée et la valeur, tu as très largement intérêt à les garder ensembles, à la manière de ce que je viens de t'expliquer.

          Tu auras, en effet, beaucoup plus facile à manipuler tes données si la relation qui existe entre les informations (la relation "abscisse / ordonné", pour le point, la relation "point / valeur" pour le noeud) est évidente, comme je viens de te les montrer, que si toutes les informations sont totalement décorrélées et réparties à  peu près n'importe où ;)

          De ce fait, si tu  as "de bonnes raisons" pour créer une relation entre trois valeurs de types double (dans le cas présent), tu as, très clairement, une excellente raison pour rendre cette relation particulièrement évidente ;)

          tbc11 a écrit:

          2) La maille :

          - Une maille est un triangle constitué par 3 points parmi les noeuds

           Humm... Pas clair, tout cela: tu nous mélange les notions de points et celles de noeuds :'(, ce qui m'incite à  te poser une question peut-être embarrassante:

          La maille s'intéresse-t-elle (d'une manière ou d'une autre) uniquement aux coordonnées des point qui composent chaque noeud ou va-t-elle avoir besoin de récupérer, à un moment ou à un autre, la valeur associée à chaque noeud?

          La réponse à cette question va, en effet, radicalement la manière dont je pourrais envisager de représenter la notion de maille:

          Si la maille ne s'intéresse qu'aux points associés aux différents noeuds, je pourrais me limiter à une représentation proche de

          struct Maille{
              /* la classe std::array est un tableau de taille
               * fixe, très utile dans le cas présent
               */
              std::array<Point,3> points;
          };

          Par contre, si la maille a aussi besoin de pouvoir accéder aux valeurs associées aux différents noeud, je devrais représenter cette notion sous une forme proche de

          struct Maille{
              std::vector<Node, 3> nodes;
          };

          Je sais, la différence a l'air minime comme cela.  Je peux pourtant t'assurer que les conséquences sont beaucoup plus nombreuses que ce que tu ne pourrait le croire d'un premier abord.

          (D'ailleurs, autant que je sache, un simple alias de type pourrait tout aussi bien faire l'affaire sur ce coup là ;) )

          tbc11 a écrit:

          - Un noeud peut être commun à plusieurs mailles

          Cela ne présente pas de problème particulier, mais, encore une fois, cela manque de précision : De combien de maille parle-t-on ici?

          Ben oui, j'aimerais savoiir au delà de quel nombre de mailles associées à  un noeud particulier, je devrai commencer à engueuler le développeur (ou l'utilisateur) pour avoir tenté d'associer trop de mailles à un noeud donné (c'est ce que l'on appelle un invariant ;) )

          tbc11 a écrit:

          - Il semble logique de définir la maille par les trois indices des points qui la constituent => 3 entiers

          "Logique", je suis pas sur, mais c'est en tout état de cause une manière "cohérente" d'envisager les choses.

          Après tout, je viens-je pas de te présenter deux extraits de code dans lesquels j'ai choisi une option différente et, pourtant tout aussi cohérente?

          Et le fait est que les trois manières de s'y prendre présenteront des avantages et des inconvénients différents (et globalement incompatibles entre eux):

          En effet, les manières que je t'ai présentée présentent un inconvénient majeur: on doit copier pas mal de données pour que cela fonctionne. Ce qui implique, outre une utilisation plus importante de la mémoire, un risque de "désynchronisation" des données si on vient à  les modifier.

          Par chance, tu nous as dit que le maillage, la position des points et des noeuds, n'était pas sensé être modifié.  Le risque de désynchronisation et donc réduit à zéro.

          Par contre, cette organisation des données nous permettra d'accéder plus rapidement aux données qui nous intéressent, vu qu'il ne sera pas nécessaire d'aller récupérer "le point (ou le noeud) qui se trouve à l'indice X du tableau" pour pouvoir en profiter.

          A l'inverse, l'utilisation d'indices permettra de limiter énormément l'utilisaiton de la mémoire, étant donné qu'un indice reste "relativement petit" par rapport aux notions de points ou de noeuds.

          Par contre, cela implique que nous devront systématiquement nous trimballer avec notre tableau de points ou de noeuds, afin d'être en mesure d'accéder aux données associées au "point (ou au noeud) qui se trouve à l'indice X du tableau", ce qui rend sans doute les manipulation "un peu moins aisées", malgré tout :p

          Comprends moi bien: je ne plaide ni pour une possibilité ni pour l'autre. Je m'efforce juste de te présenter un point de vue plus "cohérent" que le tien dans le sens où il s'efforce de fournir une analyse strictement factuelle des différentes possibilités.

          Ne sachant pas quels sont tes besoins ou tes souhaits, je me contente de te laisser le choix ;)

          tbc11 a écrit:

          - La maille peut constituer un objet en soi, car pas mal de méthodes relèvent du "niveau maille" : calcul de l'air du triangle, du cercle circonscrit, statuer si un point donné par l'utilisateur est dans le triangle ou en dehors, ...

          A vrai dire, il n'y a pas que la maille qui ait cet honneur. J'espère que tu en es convaincu depuis le début de mon intervention.

          Pour faire simple, chaque fois que tu utilise un nom (ou un groupe nomminal) dans ton analyse des besoins, tu devrais créer une notion équivalente dans ton code.  Que ce soit une variable (Maille laMailleQuiMinteressePourLinstant; ) ou un type de donnée (struct Point{/* ... */};).

          La facilité avec laquelle tu pourras corriger ou faire évoluer ton programme est à ce prix ;)

          tbc11 a écrit:

          3) Le maillage :

          - C'est l'ensemble des mailles.

          Ah, ca, ce sera en effet vachement utile...

          tbc11 a écrit:

          - Contrairement aux noeuds, l'accès direct à une maille donnée ne présente pas d'intérêt : pour statuer dans quelle maille se situe un point donné par l'utilisateur, il faut balayer toutes les mailles jusqu'à temps trouver la bonne.

          Ou alors, je m'efforce encore une fois d'élragir tes horizons, tu te dis que, si chaque maille est -- effectivement -- d'office associée à trois noeuds, cela veut sans doute dire que chaque noeud est potentiellement associé à  une maille ... ou plus.

          ( C'est la raison pour laquelle je te demandais plus haut combien de mailles nous pouvions associer à chaque noeud ;) )

          Tu pourrais alors mettre en place un système "bi directionnel", qui te permettrait, à partir d'une maille donnée, de récupérer les noeuds qu'elle utilise et, par ailleurs, qui te permettrait, à partir d'un noeud donné, de connaitre toutes les mailles qui y font référence.

          Et ca, ca t'ouvriais un champs quasi infini, car cela te permettrait -- entre autre -- de déterminer le chemin que tu dois suivre, à  partir d'un noeud donné, pour accéder à un autre noeud aussi écarté puisse-t-il être sur base des mailles qui te permettent de passer "de proche en proche" d'un noeud à l'autre.

          Je ne prétends pas que ce soit nécessaire, ne connaissant pas l'ensemble de tes souhaits.  Je dis juste que c'est une possibilité qui pourrait te faire gagner pas mal de temps pour la recherche de "noeuds adjacents".

          tbc11 a écrit:

          - D'où l'idée de constituer une classe maillage, avec pour attribut essentiel la liste des mailles (il y en a d'autres dont je ne parle pas ici)

          Ben oui, la notion de maillage est essentielle, elle doit donc apparaitre d'une manière ou d'une autre dans ton code.  Cependant, je ne suis pas sur que tu aies forcément besoin d'une classe pour cela: un simple alias de type proche de

          using Maillage = std::vector<Maille>;
          /* et, là où tu as besoin du maillage, une variable proche de
           */
          Maillage lesMailles;

          pourrait -- a priori -- parfaitement faire l'affaire ;)

          tbc11 a écrit:

          Du coup, où mettre les noeuds ?

          Ben, étant donné que ta logique va sans doute faire des "allers-retours" entre les noeud et les maille, la position "logique" pour ta liste de noeuds est sans doute ... jute à coté de ton maillage ;)

          tbc11 a écrit:

          Solution 1 : comme les noeuds dictent le maillage (modifier un noeud implique de refaire le maillage), il serait logique d'inclure les 3 vectors des noeuds comme attributs (constitutifs) du Maillage.

          Mais le hic est que les méthodes de la classe Maille doivent accéder aux coordonnées des noeuds (donc à ces vectors), et ce, des dizaines de milliers de fois. Procéder par accesseur risque d'être catastrophique au niveau des temps d'exécution.

          Mais, bon dieu, pourquoi voudrais tu trois vecteurs de noeud.  Un seul devrait suffir dés le moment où la notion de noeud est définie ;)

          Et, pour le reste, effectivement non: Dans le meilleur des cas, ton maillage n'est qu'un utilisateur de ta liste de noeuds.

          Tu as donc deux possibilités:

          • ou bien tu  transmet la liste de noeuds au maillage à chaque fois qu'il en a besoin pour travailler
          • ou bien tu fournis une référence sur la liste de noeuds, qui sera stockée "en interne" par le maillage, mais qu'il ne pourra pas modifie

          tbc11 a écrit:

          Solution 2 : déclarer ces 3 vectors publics. Mais "ça fait sale" => pas très POO... sachant qu’ils seraient accessibles (et modifiables) par l'utilisateur (niveau main) ! Et ça, je n’en veux pas.

          Parce que tu crois sans doute que cela ferait plus propre et plus "POO" de fournir ta liste de noeud comme membre privé de ton maillage et d'exposer des fonctions comme setListeDeNoeuds et getListeDeNoeuds?

          De toutes manières, je t'ai dit que ton maillage n'est que l'utilisateur de ta liste de noeuds. Tu dois donc trouver le propriétaire de cette dernière.

          Autrement, tu donneras à ton maillage une responsabilité qu'il n'a absolument pas à avoir: celle de s'occuper de la gestion des noeuds.

          tbc11 a écrit:

          Solution 3 : Solution 1 en déclarant la classe Maille amie de la classe Maillage.

          Ca résoud mon problème, mais la littérature dit que l'usage de "friend" révèle souvent un défaut de conception des classes.

          Et la littérature a parfaitement raison...

          tbc11 a écrit:

           Dans mon cas, j'ai du mal à voir lequel.

          Qu'est-ce que j'ai raté ? Quelqu'un aurait-il une meilleure idée de définition de classes ?

          Tu t'es, tout simplement, arrêté un peu trop vite dans ta réflexion:

          Tu est parti de la notion de point , tu as continué en décrivant la notion de noeuds(au fait, tu n'as pas décrit la notion de "liste de points" qui pourrait s'avérer utile en attendant de créer les noeud), puis celle de maille pour terminer avec la notion de maillage.

          Tu as, tout simplement, oublier de prévoir un "chef d'orchestre" pour mettre "le tout en musique", pour pouvoir gérer les différents éléments et leur donner les ordres "qui vont bien".

          Si tu  ajoutes cette notion (qui est en réalité très proche de ce que j'appelais "propriétaire" un peu plus haut), tu te rendras sans doute compte que tout devient d'un seul coup beaucoup plus facile ;)

          -
          Edité par koala01 10 septembre 2020 à 17:09:37

          • 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
            10 septembre 2020 à 17:22:02

            OK - merci à vous deux pour vos réponses.

            • Je reste sur des objets, car ils me permettent de ne manipuler (au niveau du main) qu'un objet et non une multitude de tableaux. Bon, j'aurais pu le faire avec des structures classiques. Mais il faut que je m'exerce :-)
            • Je reste avec une list, car lors de la constitution du maillage, je ne vais pas arrêter de créer, de détruire des mailles existantes et de recréer de nouvelles mailles.
            • Je pars donc sur un accesseur inline
            • Partager sur Facebook
            • Partager sur Twitter
              10 septembre 2020 à 17:57:05

              > Je reste avec une list, car lors de la constitution du maillage, je ne vais pas arrêter de créer, de détruire des mailles existantes et de recréer de nouvelles mailles.

              Cela n'aura d'importance que si tu continues à exploiter des références (/pointers/itérateurs) sur des mailles qui existaient avant l'ajout ou la suppression d'autres mailles.

              Si tu n'as pas ces besoins, tu pourras expérimenter à termes de jolis speedup en passant à des vecteurs.

              • Partager sur Facebook
              • Partager sur Twitter
              C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                10 septembre 2020 à 19:44:34

                Globalement, l'approche me semble cohérente. C'est une approche ultra classique en 3D d'avoir une liste de points et une liste de triangles constitués des index des points.

                Je vais faire court, il y a deja trop a lire dans cette discussion :D

                lmghs a écrit:

                Quand on fait du numérique intensif et que l'on veut des perfs, ce qui marche, ce sont les structs of Arrays (SoA), et l'objet, c'en est l'antithèse. Et du coup cela peut avoir des conséquences profondes sur le design. Visiblement, tu sembles déjà parti dans cette direction.

                C'est plus complexe que ca a ma connaissance. Je ne pense pas que l'on puisse dire dans l'absolu que les SoA sont meilleurs que les AoS. Ca dépend énormément des algos utilisés derrière.

                Je me souviens vaguement d'un papier qui passer d'un SoA a un AoS (ou l'inverse, je sais plus) en plein milieu du traitement, parce que le coût de la réorganisation était moins chère qu'utiliser une structure non optimale en fonction des algos.

                On peut par exemple facilement imaginer que les 2 coordonnées vont être utilisées en même temps et auront intérêt à être dans la meme structure, et que la 3eme valeur sera dans un array indépendant.

                eugchriss a écrit:

                C'est vrai en théorie, mais en pratique tu n'en sauras rien tant que tu ne l'auras pas testé, à cause (grâce à) des optimisations faites par le compilateur.

                Pour faire simple: Essaie d'abord d'avoir un truc qui fonctionne et seulement ensuite tu essaieras de l'optimiser.

                C'est ce qui justifie le message de eugchriss : c'est ultra diffile (et pénible) de faire une analyse théorique sur les performances réelles des algos, si on prend en compte les particularités des données, des algos, des caches, etc. Et donc on va souvent préférer une approche pragmatique : on fait, on mesure, on corrige et on recommence.

                La conséquence directe, c'est que le design doit viser avant tout à avoir un code qui sera facilement maintenable, pour être modifié en fonction des optimisations que l'on va apporter au cours du temps. Un design qui virait dans un premier temps les performances est souvent une mauvaise approche, parce que ca sera tres rarement le design optimal et les modifications vont être un cauchemar.

                lmghs a écrit:

                - tu as avoir des entités qui vont faire des choses pour toi. Et quand je parle de faire, c'est vraiment de réaliser des transformations, et pas juste de stocker des données et donner un accès à ces dernières. C'est ~ l'abstraction.

                Quand on a compris cette approche d'optimisation par profiling-corrections, on comprend mieux le but de l'encapsulation et des abstractions en termes de qualité logiciel : séparer la partie public "fixe" (si on change cette partie, on doit mettre à jour tout le code utilisateur, ce qui a un coût) et la partie privée (qui sera plus locale et que l'on va pouvoir changer plus souvent et plus facilement).

                Du coup, la réponse à certaines questions est évidente :

                - avoir des vector public : ca ne respecte pas l'encapsulation

                - utiliser friend : ca respecte l'encapsulation (en limitant qui peut avoir accès aux détails d'implémentation)

                C'est pour cela aussi qu'on dit souvent qu'il faut concevoir les classes en fonction des services et pas des données internes. Quand tu écris : 

                class Maille
                {
                private :
                int m_i0;  // Indice du 1er sommet dans le tableau de noeuds
                int m_i1;  // Indice du 2eme sommet dans le tableau de noeuds
                int m_i2;  // Indice du 3eme sommet dans le tableau de noeuds
                etc...
                };

                Tu conçois ta classe en terme de sa représentation interne, qui peut changer en fonction de tes choix d'implémentation (qui peuvent changer).

                (Il faudrait peut etre mieux utiliser des std::vector<T>::iterator peut etre ?)

                Si tu écris par exemple : 

                class Maille {
                public:
                    const Point& p1() const;
                    const Point& p2() const;
                    const Point& p3() const;
                };

                Alors tu dis que ce qui t'intéresse, c'est qu'une maille est constituée de 3 points, peu importe la representation interne. Ça peut être un SoA, un AoS, des vector ou des list, des buffer sur le GPU ou sur le SSE, etc. Osef.

                Pour les algos, ca sera pareil, tu auras une partie publique fixe et une partie interne privée et qui dépend de l'implémentation. Si tu changes la structure de données, tu devrais probablement changer aussi l'implémentation de ton algo. (Il est parfois possible d'éviter cela, via des templates, des classes de traits, etc).

                • Partager sur Facebook
                • Partager sur Twitter
                  10 septembre 2020 à 19:45:43

                  tbc11 a écrit:

                  OK - merci à vous deux pour vos réponses.

                  • Je reste sur des objets, car ils me permettent de ne manipuler (au niveau du main) qu'un objet et non une multitude de tableaux. Bon, j'aurais pu le faire avec des structures classiques. Mais il faut que je m'exerce :-)

                  En C++, il n'y a qu'une seule différence entre le mot clé struct et le mot clé class et il se fait qu'elle est tellement minime que nous pourrions tout simplement décider de ne pas en parler:

                  Il s'agit de la visibilité appliquée par défaut aux différents éléments du type que l'on définit.Elle sera public pour le le mot clé struct et private pour le mot clé class.

                  En dehors de cette différence, il n'y a absolument rien que tu puisse faire à l'aide d'un des deux mots clé que tu ne puisse pas faire avec l'autre.

                  Tu ne feras donc pas "mieux de l'objet" en utilisant le mot clé class plutôt qu'en utilisant le mot clé struct ;).

                  Ce qui te permettra de faire "mieux de l'objet", c'est le respect "au pied de la lettre" des règles issues de

                  • La loi de Déméter
                  • les principes SOLID (dont seul le L correspond en réalité à un principe exclusivement destiné à l'orienté objet)

                   Alors, bien sur, tu as sans doute été bassiné avec le principe d'encapsulation.  Et, de fait, ce principe est une conséquence directe de la loi de Déméter.

                  Le problème, c'est que ce principe est souvent mal compris, y compris par les développeurs.  Ce qui fait qu'il est souvent mal expliqué aux débutants.

                  Tu m'excuseras donc de partir du principe défavorable que tu n'as, en fait, pas mieux compris ce principe que la personne qui te l'a expliqué, ne sachant pas qui te l'as expliqué (à moins que tu n'aie lu mon livre, auquel cas, tu devrais pouvoir l'avoir compris correctement :D ) ;)

                  Donc, une encapsulation "correcte" ne va pas se contenter de mettre les données membres dans l'accessibilité private, surtout, si c'est pour fournir un couple d'accesseur (getXXX, qui peut encore se justifier dans certaines circonstances) et de mutateur (setXXX, qui est une pure aberration du point de vue de l'encapsulation) pour chaque donnée membre mise en accessibilité privée.

                  Le principe de l'encapsulation est, en effet, de permettre à l'utilisateur d'une donnée de la manipuler sans qu'il n'ai besoin de s'inquiéter ni de la manière dont cette donnée est représentée ni de la valeur des différents éléments qui la compose.

                  A ce titre, je ne sais pas si tu connais un tout  petit peu le C, mais la structure FILE est l'exemple même de ce que l'on peut appeler une "donnée parfaitement encapsulée".

                  Cela peut te paraitre bizarre que je te parle d'une structure C alors qu'on t'a sans doute tout autant bassiné avec le fait que l'encapsulation était l'un des principes fondateurs de l'orienté objets.  Et pourtant, le fait est que l'encapsulation n'est absolument pas propre à l'orienté objets (mais je n'en parlerai que si tu me poses la question ;) ) et que cette structure est merveilleusement encapsulée car

                  • On sait ce que cette structure représente : son nom l'indique assez clairement, elle représente la notion de fichier
                  • on sait comment la manipuler: au travers de fonction comme fopen, fread, fwrite ou fclose (et toutes celles que je n'aurai pas cité)
                  • et pourtant personne (*) ne sera capable de me dire quelles données elle regroupe pour nous permettre tout cela

                  (*) du moins, pas sans avoir analysé le code source de la bibliothèque du C pendant plusieurs heures et sans doute suivi des dizaines de définition de symboles destiné au préprocesseur.

                  Donc, le principe de l'encapsulation est en réalité de fournir une "notion" (une classe, une structure, une énumération ou même un alias de type si cela nous facilite la vie) à laquelle nous associerons "un certain nombre" de "services" destinés à permettre la manipulation de cette notion par l'utilisateur, sans qu'il n'ait à  s'inquiéter de quoi que ce soit ni à  se poser de question.

                  Le but étant, au final, étant de l'empêcher autant que possible de faire une connerie (chose malheureusement très fréquente) lorsqu'il manipule cette notion.

                  Les services peuvent être essentiellement de deux natures différentes, car ce peut être:

                  • une question que l'on souhaite poser à la donnée et à laquelle elle devra répondre ou
                  • un ordre que l'on souhaite donner à la donnée, et à laquelle elle devra obéir (**)

                  (**) il va de soi que la donnée ne pourra obéir "valablement" à un ordre quelconque que si toutes les conditions sont réunies pour que le résultat obtenu au travers de cet ordre corresponde au résultat auquel on est en droit de s'attendre en donnant cet ordre.  J'en parlerai peu-être un peu plus loin ;)

                  Alors, bien sur, tu me diras que, si je veux pouvoir poser une question à ma donnée, je me retrouve ** forcément ** à fournir des accesseurs. 

                  Oui et non: les accesseurs sont -- effectivement -- l'un des possibilités qui nous sont offertes pour répondre à certaines des questions que nous pourrions poser à notre donnée.

                  De cette manière, si j'ai une classe Personne et que je veux lui demander son nom, il semble "logique" d'avoir un accesseur qui me permette de l'obtenir, nous sommes bien d'accord là dessus.

                  Par contre, si j'ai une classe Voiture, qui contient (c'est préférable pour lui permettre de fonctionner :P) une donnée de type Reservoir, la loi de Déméter nous dit que je n'ai aucun besoin de connaitre le type Reservoir pour pouvoir être en mesure de manipuler ma classe Voiture.

                  Et, de fait, il semblerait aberrant que je me retrouve à devoir écrire un code proche de

                  Voiture maVoiture; // Ca, c'est ma voiture perso
                  /* je veux savoir combien de km je peux  encore parcourir
                   * avant de tomber en panne d'essence
                   * j'ai donc besoin de la quantité d'essence qu'il me reste
                   */
                  double quantite = maVoiture.getReservoir().getQuantiteRestante();
                  /* et de ma consommation moyenne actuelle */
                  double consommation = maVoiture.getConsommationCourante();
                  /* Heuu ... C'est quoi encore exactement la formule qui permet
                   * de calculer l'autonomie restante????
                   */

                  Les commentaires t'indiquent exactement les limites de cette manière de s'y prendre: si le type qui manipule une donnée de type Voiture a oublié la formule permettant de calculer l'autonomie, ou s'il fait -- plus vraisemblablement -- une erreur dans l'application de cette formule, nous risquons  de nous retrouver en panne d'essence au beau milieu de nulle part, sans aucune aide à espérer ou à attendre :p

                  Je ne vais donc pas permettre à  l'utilisateur d'accéder directement au réservoir.  Il n'y a -- de toutes manières -- que mon garagiste qui devra éventuellement pouvoir y accéder dans des circonstances très particulières.

                  Par contre, je vais donner à ma voiture le moyen de répondre à la question que tout le monde se pose : quelle est, à ce moment précis, l'autonomie restante?

                  De la même manière, si tu veux  faire le plein du réservoir de ta voiture, qu'est ce qui te semble le plus logique?  Est-ce de partir sur une logique proche de

                  Voiture maVoiture; // on a toujours ma voiture perso
                  /* j'ai besoin de la quantité de carburant qu'il reste
                   * dans le réservoir
                   */
                  double quantitéCourante = maVoiture.getReservoir().getQuantiteRestante();
                  /* il faut aussi que je me rappelle de la capacité totale
                   * du réservoir
                   */
                  couble capacite = maVoiture.getReservoir().getCapacite();
                  /* je peux maintenant calculer la quantité de carburant à
                   * ajouter dans mon réservoir
                   */
                  double aAjouter = quantiteCourante - capacite; / Heuu tu es sur????
                  /* Et je dois mettre cette quantité dans mon réservoir... 
                   */
                  maVoiture.getReservoir().ajouter(aAjouter); ///oupss... ajouter une quantité négative???

                  Parce que là, je ne sais pas si tu as remarqué, mais j'ai interverti deux données (par distraction, sans doute), si bien que mon moteur, que j'aurai pris soin de couper avant de faire le plein, ben, il n'a que très peu de chance de redémarrer, surtout s'il restait moins de la moitié de carburant dans mon réservoir :p .

                  Ou bien est-il plus logique d'avoir une sorte de "boite noire" fournie par la voiture qui me permette d'écrire un code proche de

                  Voiture maVoiture; // on a toujours ma voiture perso
                  double quantiteAjoutee = maVoiture.faireLePlein();

                  (en partant -- bien sur -- du principe que la fonction faireLePlein permet d'obtenir le résultat escompté, à savoir, la quantité de carburant qui aura été ajoutée ;) )

                  Et le fait est que si tu n'as déjà pas jugé utile de fournir un accesseur  sur une donnée membre, il n'y a -- très certainement -- absolument aucune raison pour que tu fournisse un mutateur.  Je présumes que nous serons bien d'accord sur ce fait ;)

                  Par contre, je t'entends déjà me demander "oui, mais, si on a décidé de fournir un accesseur (par exemple dans le cas de la classe Personne), pourquoi ne pas y ajouter un mutateur?'

                  Ben, posons nous la question de s'avoir si un mutateur serait utile.  Et, pour ma part, je serais d'avis que la personne nait avec un nom (et un prénom) et qu'elle va -- a priori -- le garder jusqu'à sa mort.  Il n'y a donc pas vraiment de raisons pour ajouter un mutateur.

                  Par contre, certains esprits chagrins feront remarquer, juste pour me contredire, que le changement de nom (voire même de sexe), ca existe. Et, de fait, il est possible -- dans certaines conditions -- de faire modifier l'état civil.

                  Seulement, je leur ferai remarquer en retour que de tels changements ne peuvent être envisagés que sur autorisation d'un "organe régulateur", et donc qu'il n'y a absolument aucune raison pour permettre au "premier quidam venu" d'effectuer ce genre de modifications.  Or, c'est exactement ce que permettraient les mutateurs.

                  Ce qui nous place exactement dans le genre de situations dans lesquels l'amitié peut -- effectivement -- être particulièrement intéressante, car elle permettra d'augmenter l'encapsulation en évitant de donner à "tout le monde" une possibilité qui ne doit être octroyée "qu' au compte goutte" ;)

                  tbc11 a écrit:

                  • Je reste avec une list, car lors de la constitution du maillage, je ne vais pas arrêter de créer, de détruire des mailles existantes et de recréer de nouvelles mailles.

                  Cela peut sembler logique, mais, pourquoi tiens tu absolument à utiliser une liste?

                  Car, la chose la plus marrante avec les std::vector, c'est que leur capacité (le nombre d'éléments qu'ils sont susceptibles de contenir) et leur taile (le nombre d'éléments qu'ils contiennent effectivement à un instant T) évoluent tout à fait séparément.  Je m'explique:

                  Quand on veut ajouter un élément, si la taille du tableau est égale à sa capacité, on va prendre des mesure pour augmenter sa capacité (généralement dans un facteur de 2, à peu près, pour éviter d'avoir à le refaire trop souvent)

                  Par contre, lorsque l'on veut retirer un élément du tableau (ce qui revient en fait à le détruire, nous sommes d'accord?) la taille va diminuer, mais on ne va pas forcément modifier la capacité du tableau, si bien que, si tu en viens à détruire quatre éléments "coup sur coup", tu as la certitude qu'il ne faudra pas augmenter la capacité de ton tableau avant d'essayer d'ajouter au moins le cinquième élément (et potentiellement même plus).

                  • 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
                    11 septembre 2020 à 0:46:54

                    Merci à tous... Tout d'abord, comme statué :

                    gbdivers a écrit:Je vais faire court, il y a deja trop a lire dans cette discussion

                    Je vais devoir faire mon marché et cela va prendre du temps. Parmi ce qui me donne à réfléchir, dans l'ordre chronologique des retours :

                    eugchriss a écrit: Comme disait l'autre : "Premature optimization is the root of all evil"

                    Sage recommandation. Reçu 5 sur 5 : je fais d'abord un truc qui marche, et je verrai s'il faut l'améliorer, et sur quel axe.

                    lmghs a écrit: - réaliser ces transformations de manière cohérente, probablement en cachant des rouages internes, mais surtout de façon à garantir des invariants. Si tes données ne sont que des agrégats sans invariants, alors elles n'ont aucune raison de devenir des objets.

                    lmghs insiste beaucoup sur ce qu'il appelle les "invariants". Dans mon problème de maillage triangulaire, j'en vois déjà quelques uns. Mais j'ai besoin de réfléchir quelques jours avant de lui répondre quelque chose qui tienne la route.

                    lmghs a écrit: Un accesseur inline ne va rien coûter en perfs. L'assembleur sera le même.

                    OK - reçu 5/5. Donc pas besoin de ma classe amie, puisque le problème de perte de performances dû aux accesseurs ne se pose pas s'ils sont inline.

                    koala01 a écrit: En C++, il n'y a qu'une seule différence entre le mot clé struct et le mot clé class et il se fait qu'elle est tellement minime que nous pourrions tout simplement décider de ne pas en parler:

                    Si, j'en vois une : l'existence de méthodes apportées par un objet, que n'apporte pas une structure classique. La discussion s'est orientée vers les attributs, et insuffisamment vers les méthodes.

                    Or, dans le cadre de mon problème de construction de maillage 2D (triangulation de Delaunay), il existe :

                    • des fonctions qui prennent tout leur sens au niveau de la maille (triangle) individuelle prise isolément, et qui ne signifient rien au niveau du maillage : par exemple, calculer le cercle circonscrit à un triangle (étape nécessaire pour l'algorithme de sélection des "bonnes" mailles) n'a de sens que pour une maille triangulaire, et n'a aucun sens au niveau du maillage qu'aucun cercle unique ne sera capable de circonscrire.
                    • et inversement, évaluer si le maillage (i.e. la façon de construire les triangles) répond aux conditions de Delaunay (condition nécessaire pour avoir un maillage capable d'une interpolation correcte - ce qui est mon but final) fait sens au niveau du maillage (i.e. de l'ensemble des triangles) et est absurde au niveau d'une maille isolée.

                    En ce sens, ces différentes fonctions "outils" sont à associer au périmètre qu'elles couvrent => la notion de méthode (inhérentes à une classe) me permet de le faire facilement et proprement.

                    koala01 a écrit:

                    tbc11 a écrit:

                    OK - merci à vous deux pour vos réponses.

                    • Je reste sur des objets, car ils me permettent de ne manipuler (au niveau du main) qu'un objet et non une multitude de tableaux. Bon, j'aurais pu le faire avec des structures classiques. Mais il faut que je m'exerce :-)

                    En C++, il n'y a qu'une seule différence entre le mot clé struct et le mot clé class et il se fait qu'elle est tellement minime que nous pourrions tout simplement décider de ne pas en parler:

                    Il s'agit de la visibilité appliquée par défaut aux différents éléments du type que l'on définit.Elle sera public pour le le mot clé struct et private pour le mot clé class.

                    En dehors de cette différence, il n'y a absolument rien que tu puisse faire à l'aide d'un des deux mots clé que tu ne puisse pas faire avec l'autre.

                    Tu ne feras donc pas "mieux de l'objet" en utilisant le mot clé class plutôt qu'en utilisant le mot clé struct ;).

                    Ce qui te permettra de faire "mieux de l'objet", c'est le respect "au pied de la lettre" des règles issues de

                    • La loi de Déméter
                    • les principes SOLID (dont seul le L correspond en réalité à un principe exclusivement destiné à l'orienté objet)

                     Alors, bien sur, tu as sans doute été bassiné avec le principe d'encapsulation.  Et, de fait, ce principe est une conséquence directe de la loi de Déméter.

                    Le problème, c'est que ce principe est souvent mal compris, y compris par les développeurs.  Ce qui fait qu'il est souvent mal expliqué aux débutants.

                    Tu m'excuseras donc de partir du principe défavorable que tu n'as, en fait, pas mieux compris ce principe que la personne qui te l'a expliqué, ne sachant pas qui te l'as expliqué (à moins que tu n'aie lu mon livre, auquel cas, tu devrais pouvoir l'avoir compris correctement :D ) ;)

                    Donc, une encapsulation "correcte" ne va pas se contenter de mettre les données membres dans l'accessibilité private, surtout, si c'est pour fournir un couple d'accesseur (getXXX, qui peut encore se justifier dans certaines circonstances) et de mutateur (setXXX, qui est une pure aberration du point de vue de l'encapsulation) pour chaque donnée membre mise en accessibilité privée.

                    Le principe de l'encapsulation est, en effet, de permettre à l'utilisateur d'une donnée de la manipuler sans qu'il n'ai besoin de s'inquiéter ni de la manière dont cette donnée est représentée ni de la valeur des différents éléments qui la compose.

                    A ce titre, je ne sais pas si tu connais un tout  petit peu le C, mais la structure FILE est l'exemple même de ce que l'on peut appeler une "donnée parfaitement encapsulée".

                    Cela peut te paraitre bizarre que je te parle d'une structure C alors qu'on t'a sans doute tout autant bassiné avec le fait que l'encapsulation était l'un des principes fondateurs de l'orienté objets.  Et pourtant, le fait est que l'encapsulation n'est absolument pas propre à l'orienté objets (mais je n'en parlerai que si tu me poses la question ;) ) et que cette structure est merveilleusement encapsulée car

                    • On sait ce que cette structure représente : son nom l'indique assez clairement, elle représente la notion de fichier
                    • on sait comment la manipuler: au travers de fonction comme fopen, fread, fwrite ou fclose (et toutes celles que je n'aurai pas cité)
                    • et pourtant personne (*) ne sera capable de me dire quelles données elle regroupe pour nous permettre tout cela

                    (*) du moins, pas sans avoir analysé le code source de la bibliothèque du C pendant plusieurs heures et sans doute suivi des dizaines de définition de symboles destiné au préprocesseur.

                    Donc, le principe de l'encapsulation est en réalité de fournir une "notion" (une classe, une structure, une énumération ou même un alias de type si cela nous facilite la vie) à laquelle nous associerons "un certain nombre" de "services" destinés à permettre la manipulation de cette notion par l'utilisateur, sans qu'il n'ait à  s'inquiéter de quoi que ce soit ni à  se poser de question.

                    Le but étant, au final, étant de l'empêcher autant que possible de faire une connerie (chose malheureusement très fréquente) lorsqu'il manipule cette notion.

                    Les services peuvent être essentiellement de deux natures différentes, car ce peut être:

                    • une question que l'on souhaite poser à la donnée et à laquelle elle devra répondre ou
                    • un ordre que l'on souhaite donner à la donnée, et à laquelle elle devra obéir (**)

                    (**) il va de soi que la donnée ne pourra obéir "valablement" à un ordre quelconque que si toutes les conditions sont réunies pour que le résultat obtenu au travers de cet ordre corresponde au résultat auquel on est en droit de s'attendre en donnant cet ordre.  J'en parlerai peu-être un peu plus loin ;)

                    Alors, bien sur, tu me diras que, si je veux pouvoir poser une question à ma donnée, je me retrouve ** forcément ** à fournir des accesseurs. 

                    Oui et non: les accesseurs sont -- effectivement -- l'un des possibilités qui nous sont offertes pour répondre à certaines des questions que nous pourrions poser à notre donnée.

                    De cette manière, si j'ai une classe Personne et que je veux lui demander son nom, il semble "logique" d'avoir un accesseur qui me permette de l'obtenir, nous sommes bien d'accord là dessus.

                    Par contre, si j'ai une classe Voiture, qui contient (c'est préférable pour lui permettre de fonctionner :P) une donnée de type Reservoir, la loi de Déméter nous dit que je n'ai aucun besoin de connaitre le type Reservoir pour pouvoir être en mesure de manipuler ma classe Voiture.

                    Et, de fait, il semblerait aberrant que je me retrouve à devoir écrire un code proche de

                    Voiture maVoiture; // Ca, c'est ma voiture perso
                    /* je veux savoir combien de km je peux  encore parcourir
                     * avant de tomber en panne d'essence
                     * j'ai donc besoin de la quantité d'essence qu'il me reste
                     */
                    double quantite = maVoiture.getReservoir().getQuantiteRestante();
                    /* et de ma consommation moyenne actuelle */
                    double consommation = maVoiture.getConsommationCourante();
                    /* Heuu ... C'est quoi encore exactement la formule qui permet
                     * de calculer l'autonomie restante????
                     */

                    Les commentaires t'indiquent exactement les limites de cette manière de s'y prendre: si le type qui manipule une donnée de type Voiture a oublié la formule permettant de calculer l'autonomie, ou s'il fait -- plus vraisemblablement -- une erreur dans l'application de cette formule, nous risquons  de nous retrouver en panne d'essence au beau milieu de nulle part, sans aucune aide à espérer ou à attendre :p

                    Je ne vais donc pas permettre à  l'utilisateur d'accéder directement au réservoir.  Il n'y a -- de toutes manières -- que mon garagiste qui devra éventuellement pouvoir y accéder dans des circonstances très particulières.

                    Par contre, je vais donner à ma voiture le moyen de répondre à la question que tout le monde se pose : quelle est, à ce moment précis, l'autonomie restante?

                    De la même manière, si tu veux  faire le plein du réservoir de ta voiture, qu'est ce qui te semble le plus logique?  Est-ce de partir sur une logique proche de

                    Voiture maVoiture; // on a toujours ma voiture perso
                    /* j'ai besoin de la quantité de carburant qu'il reste
                     * dans le réservoir
                     */
                    double quantitéCourante = maVoiture.getReservoir().getQuantiteRestante();
                    /* il faut aussi que je me rappelle de la capacité totale
                     * du réservoir
                     */
                    couble capacite = maVoiture.getReservoir().getCapacite();
                    /* je peux maintenant calculer la quantité de carburant à
                     * ajouter dans mon réservoir
                     */
                    double aAjouter = quantiteCourante - capacite; / Heuu tu es sur????
                    /* Et je dois mettre cette quantité dans mon réservoir... 
                     */
                    maVoiture.getReservoir().ajouter(aAjouter); ///oupss... ajouter une quantité négative???

                    Parce que là, je ne sais pas si tu as remarqué, mais j'ai interverti deux données (par distraction, sans doute), si bien que mon moteur, que j'aurai pris soin de couper avant de faire le plein, ben, il n'a que très peu de chance de redémarrer, surtout s'il restait moins de la moitié de carburant dans mon réservoir :p .

                    Ou bien est-il plus logique d'avoir une sorte de "boite noire" fournie par la voiture qui me permette d'écrire un code proche de

                    Voiture maVoiture; // on a toujours ma voiture perso
                    double quantiteAjoutee = maVoiture.faireLePlein();

                    (en partant -- bien sur -- du principe que la fonction faireLePlein permet d'obtenir le résultat escompté, à savoir, la quantité de carburant qui aura été ajoutée ;) )

                    Et le fait est que si tu n'as déjà pas jugé utile de fournir un accesseur  sur une donnée membre, il n'y a -- très certainement -- absolument aucune raison pour que tu fournisse un mutateur.  Je présumes que nous serons bien d'accord sur ce fait ;)

                    Par contre, je t'entends déjà me demander "oui, mais, si on a décidé de fournir un accesseur (par exemple dans le cas de la classe Personne), pourquoi ne pas y ajouter un mutateur?'

                    Ben, posons nous la question de s'avoir si un mutateur serait utile.  Et, pour ma part, je serais d'avis que la personne nait avec un nom (et un prénom) et qu'elle va -- a priori -- le garder jusqu'à sa mort.  Il n'y a donc pas vraiment de raisons pour ajouter un mutateur.

                    Par contre, certains esprits chagrins feront remarquer, juste pour me contredire, que le changement de nom (voire même de sexe), ca existe. Et, de fait, il est possible -- dans certaines conditions -- de faire modifier l'état civil.

                    Seulement, je leur ferai remarquer en retour que de tels changements ne peuvent être envisagés que sur autorisation d'un "organe régulateur", et donc qu'il n'y a absolument aucune raison pour permettre au "premier quidam venu" d'effectuer ce genre de modifications.  Or, c'est exactement ce que permettraient les mutateurs.

                    Ce qui nous place exactement dans le genre de situations dans lesquels l'amitié peut -- effectivement -- être particulièrement intéressante, car elle permettra d'augmenter l'encapsulation en évitant de donner à "tout le monde" une possibilité qui ne doit être octroyée "qu' au compte goutte" ;)

                    tbc11 a écrit: Je reste avec une list, car lors de la constitution du maillage, je ne vais pas arrêter de créer, de détruire des mailles existantes et de recréer de nouvelles mailles. Cela peut sembler logique, mais, pourquoi tiens tu absolument à utiliser une liste?

                    D'après ce que j'ai (peut-être mal) compris :

                    • Un tableau présente l'avantage d'accéder directement à une composante donnée, sans se farcir le parcours du 1er élément à celui qui nous intéresse. En contrepartie, il impose la contiguité en mémoire des éléments stockés, ce qui est une contraignant, surtout que mes objets Maille peuvent être encombrants.
                    • Une liste n'impose pas cette contrainte de contiguité des éléments en mémoire. En revanche, pour accéder à l'élément voulu, il faut la parcourir du début jusqu'à ce qu'on trouve celui qui convient.

                    Donc utiliser un tableau (ou un vector) n'est intéressant que si l'indice de l'élément a une signification : par exemple, si je veux accéder à l'élément n° 47, c'est que j'ai une bonne raison de le faire, parce que je sais ce que n°47 signifie (dans un critère d'ordre).

                    En revanche, si l'indice n'est que l'odre d'arrivée de l'élément dans un "catalogue", l'avantage du tableau disparaît : par exemple, je n'ai aucun algorithme pour déterminer à quelle page de l'annuaire de Haute-Loire figure M. Jean TARTEMPION. Je n'ai pas d'autre choix que de parcourir l'annuaire jusqu'à le trouver.

                    Dans mon problème de maillage :

                    • Le numéro d'un noeud signifie quelque chose : si mon triangle est constitué des noeuds (17, 53, 147) et que je veux en récupérer les coordonnées (abscisse et ordonnée), je vais lire les tableaux (ou vectors) de noeuds aux indices correspondants => dans ce cas, le concept de tableau m'est utile.
                    • En revanche, le numéro d'une maille ne me sert à rien : lorsque je voudrai interpoler ma fonction (mon but final, je le répète) sur un point quelconque du plan, il va me falloir déterminer quelle maille contient ce point. Aucun algorithme prédicitf n'existe. Il faut essayer toutes les mailles jusqu'à temps trouver celle qui convient. Le concept de tableau ne m'aidant pas, je ne suis donc pas prêt à accepter la contrepartie de contiguité du stockage en mémoire en l'échange d'une propriété qui ne me sert à rien.
                    • Partager sur Facebook
                    • Partager sur Twitter
                      11 septembre 2020 à 8:27:08

                      tbc11 a écrit:

                      koala01 a écrit: En C++, il n'y a qu'une seule différence entre le mot clé struct et le mot clé classet il se fait qu'elle est tellement minime que nous pourrions tout simplement décider de ne pas en parler:

                      Si, j'en vois une : l'existence de méthodes apportées par un objet, que n'apporte pas une structure classique. La discussion s'est orientée vers les attributs, et insuffisamment vers les méthodes.

                      Et bien, justement, non... Il n'y a rien qui t'empêche, en C++, de définir une fonction membre à une structure.  D'ailleurs, si tu y regarde d'un peu plus près, tu te rendras compte que presque toutes les fonctionnalités de la bibliothèque standard sont définies en tant que structures, et qu'elles définissent pourtant des fonctions membres ;)

                      Et, si tu n'as pas trop envie de plonger dans le code des fichiers de la bibliothèque standard tu peux toujours essayer avec ce code pour t'en convaincre:

                      #include <iostream>
                      struct MyStruct{
                          void print() const{
                              std::cout<<"I'm a struct, but I've a member function\n";
                          }
                      };
                      
                      int maint(){
                          MyStruct s;
                          s.print();
                          return 0;
                      }

                      Si tu essaye de le compiler, tu te rendras compte qu'il compile sans aucun problème et sans aucun avertissement, même si tu  pousse les warning à leur maximum.

                      Et, si tu essayes d'exécuter le programme obtenu, tu verras bel et bien l'affichage de "I'm a struct, but I've a member function", exactement comme tu étais en droit de t'y attendre ;).

                      Car il faut bien te rendre compte que ce que tu appelle "méthode" et que je préfère appeler "fonction membre" (en C++, du moins), ca fonctionne exactement comme les fonctions tout à fait classiques, et ne présente que  quatre légères différences par rapport à elles:

                      • Bien sur, elle dépendent d'une classe ou d'une structure spécifique, et il faut donc utiliser le nom pleinement qualifié pour en parler (dans mon code, ce serait void MyStruct::print() const )
                      • On peut les qualifier const (comme je l'ai fait pour la fonction print() )pour indiquer au compilateur qu'elles ne sont pas sensées modifier l'instance de la classe ou de la structure à partir de laquelle elles ont été appelées
                      • on peut les qualifier de virtual pour indiquer au compilateur que l'on envisage d'en modifier le comportement dans les classes ou les structures dérivées
                      • le compilateur ajoute "sans rien dire à personne", de manière tout à fait automatique et transparente, un paramètre nommé this, qui est de type "pointeur sur la structure (ou sur la classe)", qui représente l'instance de la structure (ou de la classe) à partir de laquelle l'instance a été appelée.

                      ET C'EST TOUT!!!

                      tbc11 a écrit:

                      D'après ce que j'ai (peut-être mal) compris :

                      • Un tableau présente l'avantage d'accéder directement à une composante donnée, sans se farcir le parcours du 1er élément à celui qui nous intéresse. En contrepartie, il impose la contiguité en mémoire des éléments stockés, ce qui est une contraignant, surtout que mes objets Maille peuvent être encombrants.
                      • Une liste n'impose pas cette contrainte de contiguité des éléments en mémoire. En revanche, pour accéder à l'élément voulu, il faut la parcourir du début jusqu'à ce qu'on trouve celui qui convient.

                      Donc utiliser un tableau (ou un vector) n'est intéressant que si l'indice de l'élément a une signification : par exemple, si je veux accéder à l'élément n° 47, c'est que j'ai une bonne raison de le faire, parce que je sais ce que n°47 signifie (dans un critère d'ordre).

                      En revanche, si l'indice n'est que l'odre d'arrivée de l'élément dans un "catalogue", l'avantage du tableau disparaît : par exemple, je n'ai aucun algorithme pour déterminer à quelle page de l'annuaire de Haute-Loire figure M. Jean TARTEMPION. Je n'ai pas d'autre choix que de parcourir l'annuaire jusqu'à le trouver.

                      Non, justement, il présente d'autres avantages...

                      Entre autres, celui d'organiser différemment les données en mémoires, à savoir que la liste va créer ses élément sous une forme telle que chaque élément est (potentiellement) mis en rapport avec l'élément qui le précède et celui qui le suis, sous une forme qui pourrait ressember à (désolé, c'est de l'ASCII Art :P)

                          |prev | element | next  <- ca, c'est un élément
                                  ^           
                                 /
                                /
                           |prev | element | next  <- voici  un deuxième élément
                                    ^        /
                                   /        /
                                  /        /
                           |prev | element |next <- mis en relation avec u troisieme

                      Alors que le tableau va s'assurer que tous les éléments seront contigus en mémoire.  C'est à  dire que le deuxième élément se trouvera exactement à  l'adresse du premier + un décalage suffisant que pour pouvoir représenter l'élément en question en mémoire.

                      Et ca, ca change un maximum de choses, entre autres, pour l'accès aux éléments.  Parce que, si je suis au début de ma liste et que je veux atteindre le 100eme élément, je vais forcément devoir passer par ... les 99 éléments qui m'en séparent, alors que si je suis au début de mon tableau et que je veux atteindre le 100eme élément, je peux "en un seul mouvement" faire un bond de 99* la taille des éléments en mémoire en étant sur que cela me permettra d'accéder au centième élément.

                      Pour la recherche, aussi: si les éléments de mon tableau sont triés, comme il ne me faut qu'un seul mouvement pour aller "où je veux" dans le tableau, je peux décider de sauter directement à l'élément qui se trouve au milieu, vérifier sa valeur, et choisir une des trois solutions qui s'offrent à moi:

                      c'est l'élément que je cherchais: je suis tranquille parce que je l'ai trouvé

                      • l'élément que je recherche est plus petit que cet élément: j'abandonne, tout simplement,  tous les éléments qui suivent celui sur lequel je suis, car je suis sur que celui que je recherche n'en fait pas partie
                      • l'élément que je recherche est plus grand que cet élément: j'abandonne, tout simplement, tous les éléments qui précèdent celui  sur lequel je suis, car je suis sur que celui que je recherche n'en fait pas partie

                      Et je recommence avec la même logique jusqu'à ce que j'aie trouvé l'élément que je recherche (ou jusqu'à ce qu'il n'y ait plus d'éléments à tester).

                      Cette pratique, qui s'appelle la dichotomie, permet de "supprimer" la moitié des possibilités à chaque tests, et donc de trouver celle que tu cherches, au pire, on log(N) où N correspond au nombre de possibilité original (pour te donner un exemple : avec 255 valeurs, je suis sur de trouver celle que je cherche en seulement ... 8 tests)

                      Il faut te dire que, de manière générale, il existe cinq structures permettant de représenter des collections (pile, file, liste, tableau et arbres binaires).  Chacune de ces structures présente des avantages, des situations dans lesquelles elles sont efficaces, et des inconvénients, des situations dans lesquelles elles sont sans doute "moins efficaces" qu'une autre.

                      La première chose à faire, c'est de choisir la bonne collection pour l'usage que tu en fera.  Cela ne changera quasiment rien à ton code, mais cela te permettra de le rendre beaucoup plus performant "d'un seul coup", et presque sans rien faire.  C'est toujours bon à prendre non?

                      tbc11 a écrit:

                      • En revanche, le numéro d'une maille ne me sert à rien : lorsque je voudrai interpoler ma fonction (mon but final, je le répète) sur un point quelconque du plan, il va me falloir déterminer quelle maille contient ce point. Aucun algorithme prédicitf n'existe. Il faut essayer toutes les mailles jusqu'à temps trouver celle qui convient. Le concept de tableau ne m'aidant pas, je ne suis donc pas prêt à accepter la contrepartie de contiguité du stockage en mémoire en l'échange d'une propriété qui ne me sert à rien.

                      Tu en es sur?

                      Et si je te donnais le moyen de trier tes maille, par exempe, en commencant par l'indice du premier point, puis sur l'indice du second et enfin sur l'indice du troisième?

                      Ne crois tu pas que tu pourrais très facilement profiter de la dicotomie pour supprimer une bonne partie des mailles à chaque fois?

                      • 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
                        13 septembre 2020 à 20:21:57

                        Bonjour à tous,

                        Je reviens sur mon problème de maillage, qui a généré beaucoup de réponses / suggestions un peu "dans tous les sens", qu'il me faut trier.

                        Ceux qui ont répondu à mes questions :

                        Eugchriss :

                        • Me confirme que, d'un point de vue conceptuel, ma mise en données n'est pas ahurissante par rapport à ce qui se fait dans les applications gérant des modèles 3D.
                        • Merci à lui. Je persiste donc avec mes nœuds, mes mailles et mon maillage. Sujet clos sur ce point.

                        lmghs :

                        • Insiste beaucoup sur le fait de ne pas faire "des objets pour le plaisir faire des objets", juste pour empaqueter des données dans une variable composite.
                        • Mais que cette classe n'apportera quelque chose que si elle garantit ce qu'il appelle "des invariants", i.e. des certitudes que l'objet est et reste utilisable, de sa création jusqu'à sa fin de vie (sa destruction).

                        J'avais décrit (au début de la discussion) ma classe "Maille" :

                        class Maille
                        {
                        private :
                        int m_i0  //Indice du 1er sommet dans le tableau de noeuds
                        int m_i1  //Indice du 2eme sommet dans le tableau de noeuds
                        int m_i2  //Indice du 3eme sommet dans le tableau de noeuds
                        etc...
                        };
                        

                        Et lmghs me demandait où sont les invariants (de la Maille) là-dedans ? Du coup, il me faut développer ce qui se cachait derrière le etc...  :

                        class Maille
                        {
                        private : 
                        int m_i0 // Indice du 1er  sommet dans le tableau de noeuds
                        int m_i1 // Indice du 2eme sommet dans le tableau de noeuds
                        int m_i2 // Indice du 3ème sommet dans le tableau de noeuds
                        double m_A  // Aire du triangle
                        double m_xc // Abscisse du centre du cercle circonscrit
                        double m_yc // Ordonnée du centre du cercle circonscrit
                        double rsq  // Carré du rayon du cercle circonscrit
                        etc...
                        };

                        La construction du maillage triangulaire va s'appuyer sur les règles de Delaunay, qui s'appuient sur le cercle circonscrit au triangle que j'essaie de former. Par ailleurs, l'aire du triangle me sera utile plus tard.

                        Pour chaque triangle, j'ai donc 7 attributs :

                        • dont 3 (les indices des noeuds des 3 sommets) sont fixés par "l'utilisateur" - ou plutôt par la couche algorithmique qui va construire le maillage
                        • et les 4 autres (les 4 double) qui découlent directement de ce choix.

                        Les calculs pour établir les 4 réels double précision, à partir des 3 sommets, ne sont pas terrifiants, mais pas immédiats et coûtent de la CPU :

                        • D'où l'intérêt de le faire dès la constitution du triangle et de les mémoriser dans la classe, afin d'éviter de les refaire à chaque fois qu'on interroge le triangle.
                        • Oui mais qui dit mémorisation implique d'assurer en permanence la cohérence entre ces 4 réels double précision et les sommets déclarés. Sinon, l'algorithme (l'utilisateur de ma classe) va se planter complètement.

                        Cette cohérence à assurer est l'invariant de la classe. Si je comprends bien le discours de lmghs :

                        • La classe "Maille" aurait un sens
                        • Pour peu que les coordonnées du centre et le rayon du cercle circonscrit (ainsi que l'aire du triangle) soient déterminés dès la construction de l'objet Maille => calculs à intégrer au constructeur, qui aurait comme arguments d'entrée les indices des 3 sommets.
                        • Et quand je détruis l'objet Maille (parce qu'elle ne m'intéresse pas), je détruis tout sans "laisser de scories".

                        Ai-je bien compris ?

                        En revanche, sur la classe Maillage (i.e. l'ensemble des Mailles) - j'ai plus de mal à voir mes "invariants". Je reviendrai vers lmghs sur ce point quand j'aurai progressé dans ma réflexion.

                        • Partager sur Facebook
                        • Partager sur Twitter
                          13 septembre 2020 à 21:38:03

                          Une piste générale : raisonner non pas sur ce que "contiennent" les classes, mais ce qu'on veut faire avec.

                          • Partager sur Facebook
                          • Partager sur Twitter
                            13 septembre 2020 à 22:06:39

                            Pour michelbillaud :

                            De ce côté-là, c'est parfaitement clair :

                            • Une classe "Maille" qui contiendra les données et fonctions (au niveau de la maille individuelle) pour :

                                           a)  supporter le processus de construction du maillage

                                           b) interpoler la fonction 2D (que je cherche à représenter) sur n'importe quel point du plan contenu dans cette maille, une fois le maillage défini.

                            • Un "machin" - qui sera un objet ou non suivant les réponses à mes questions - pour :

                                         a) déterminer le maillage (en tant qu'assemblage de triangles) répondant aux conditions de Delaunay

                                         b) une fois le maillage défini et validé, déterminer quelle maille contient n'importe quel point du plan où l'utilisateur désire interpoler.

                            Pour plus d'informations sur la méthodologie de maillage du plan par des triangles - indépendamment de l'implémentation dans un langage de programmation :

                            http://annabellecollin.perso.math.cnrs.fr/Mesh/ProjetGenerationMaillageDelaunay.pdf

                            -
                            Edité par tbc11 13 septembre 2020 à 22:19:04

                            • Partager sur Facebook
                            • Partager sur Twitter
                              13 septembre 2020 à 22:18:03

                              Ca a pas l'air si clair, d'après les indices

                              • "qui contiendra ce qu'il faut"
                              • "n'importe quel point dans cette maille" => il y a une fonction qui, à partir d'une maille, énumère ces points ?
                              • toutes les fonctions impliquées (si elles y sont toutes, en effet, on n'a pas besoin d'autre chose :-) ). Mais quelles fonctions impliquées ?
                              • un machin pour déterminer quelle maille contient le point. C'est le maillage, le machin ? 

                              -
                              Edité par michelbillaud 13 septembre 2020 à 22:21:23

                              • Partager sur Facebook
                              • Partager sur Twitter
                                13 septembre 2020 à 22:34:31

                                Je pense que je ne me suis pas bien fait comprendre :

                                • Je cherche à interpoler une fonction 2D dans tout le plan,
                                • sur la base d'une connaissance partielle de cette fonction en des points discrets sur lesquels je connais la valeur de cette fonction.

                                Il me faut pour cela discrétiser correctement l'espace (au sens des critères de Delaunay - cf. le lien donné dans mon message). Le programme que je cherche à faire est celui qui va supporter l'algorithme de de construction de cette discrétisation.

                                Il n'y a donc pas de "il y a une fonction qui, à partir d'une maille, énumère ces points" : le choix du(des) point(s) où interpoler la fonction relève de l'utilisateur, en fonction de ce qu'il souhaite calculer - et absolument pas de l'algorithme de maillage de l'espace.

                                Quand je parle du "machin", c'est bien du maillage dont il s'agit, c'est à dire de la discrétisation de Delaunay (l'ensemble des triangles qui conviennent) pour faire le travail d'interpolation.

                                • Partager sur Facebook
                                • Partager sur Twitter
                                  13 septembre 2020 à 22:34:50

                                  tbc11 a écrit:

                                  Bonjour à tous,

                                  Je reviens sur mon problème de maillage, qui a généré beaucoup de réponses / suggestions un peu "dans tous les sens", qu'il me faut trier.

                                  Ceux qui ont répondu à mes questions :

                                  Eugchriss :

                                  • Me confirme que, d'un point de vue conceptuel, ma mise en données n'est pas ahurissante par rapport à ce qui se fait dans les applications gérant des modèles 3D.

                                  Elle n'est peut être pas "aberrante", à condition de définir clairement les responsabilités de chaque parties.

                                  Ce point particulier (le fait de définir clairement les services que tu attends de tes différentes classes) mériterait clairement que tu prenne le temps d'y penser et de nous présenter tes conclusion parce que

                                  tbc11 a écrit:

                                  • Merci à lui. Je persiste donc avec mes nœuds, mes mailles et mon maillage. Sujet clos sur ce point.

                                  On n'a rien contre l'idée d'avoir des notions de point, noeud, maille et maillage dans dont code. Bien au contraire! Ce sont des notions qui apparaissent dans ton analyse des besoins et qui méritent donc d'apparaitre, sous une forme ou une autre, dans ton code.

                                  Seulement, tu confonds le fait d'encapsuler (j'ai l'impression que je pourrais rajouter "quoi qu'il en coute") les données avec le fait de faire de l'orienté objets.

                                  Je peux te rassurer: tu n'es certainement pas le seul à faire cette confusion. Car le fait est que, tant que tu ne feras pas intervenir les notions d'héritage (public) et de polymorphisme (d'inclusion), tu ne feras pas réellement intervenir la notion d'orienté objets.

                                  En effet, le principe de base de l'orienté objets, c'est le principe de substitution de Liskov (mieux connu sous le petit nom de LSP pour Liskov Substitution Principle).

                                  Si tu te contente de profiter de la possibilité de réduire l'accessibilité des données (en les plaçant dans une accessibilité private, par exemple) et de celle de créer des fonctions membres, tu te contente de faire ... de l'encapsulation.

                                  Et j'insiste sur le fait  que, pour que l'encapsulation puisse être jugée correcte, il ne suffit pas de fournir simplement un accesseur et un mutateur sur les données en question: il faut n'exposer qu'un nombre minimal de services pour éviter à l'utilisateur de tes classes de faire une connerie lorsqu'il les manipule ;)

                                  tbc11 a écrit:

                                  Bonjour à tous,

                                  Je reviens sur mon problème de maillage, qui a généré beaucoup de réponses / suggestions un peu "dans tous les sens", qu'il me faut trier.

                                  Ceux qui ont répondu à mes questions :

                                  Eugchriss :

                                  • Me confirme que, d'un point de vue conceptuel, ma mise en données n'est pas ahurissante par rapport à ce qui se fait dans les applications gérant des modèles 3D.
                                  • Merci à lui. Je persiste donc avec mes nœuds, mes mailles et mon maillage. Sujet clos sur ce point.

                                  lmghs :

                                  • Insiste beaucoup sur le fait de ne pas faire "des objets pour le plaisir faire des objets", juste pour empaqueter des données dans une variable composite.

                                  Et il a bien raison, nous nous rejoignons d'ailleurs tous les deux sur ce point. Je me permet juste d'insister un peu plus que lui sur le fait que, si tu te contente d'encapsuler tes données, tu ne fais pas -- à proprement parler -- de l'orienté objets ;)

                                  tbc11 a écrit:

                                  lmghs:

                                  • Mais que cette classe n'apportera quelque chose que si elle garantit ce qu'il appelle "des invariants", i.e. des certitudes que l'objet est et reste utilisable, de sa création jusqu'à sa fin de vie (sa destruction).

                                  J'avais décrit (au début de la discussion) ma classe "Maille" :

                                  class Maille
                                  {
                                  private :
                                  int m_i0  //Indice du 1er sommet dans le tableau de noeuds
                                  int m_i1  //Indice du 2eme sommet dans le tableau de noeuds
                                  int m_i2  //Indice du 3eme sommet dans le tableau de noeuds
                                  etc...
                                  };
                                  

                                  Et lmghs me demandait où sont les invariants (de la Maille) là-dedans ? Du coup, il me faut développer ce qui se cachait derrière le etc...  :

                                  class Maille
                                  {
                                  private : 
                                  int m_i0 // Indice du 1er  sommet dans le tableau de noeuds
                                  int m_i1 // Indice du 2eme sommet dans le tableau de noeuds
                                  int m_i2 // Indice du 3ème sommet dans le tableau de noeuds
                                  double m_A  // Aire du triangle
                                  double m_xc // Abscisse du centre du cercle circonscrit
                                  double m_yc // Ordonnée du centre du cercle circonscrit
                                  double rsq  // Carré du rayon du cercle circonscrit
                                  etc...
                                  };

                                  Même avec ces informations supplémentaires, nous ne voyons aucun invariants apparaitre.

                                  A priori, tes classes ont -- du moins pour la plupart -- sémantique de valeur.

                                  On souhaiterait donc, à tout le moins, voir quatre invariants "de base" apparaitre, à savoir:

                                  • la possibilité de créer une instance de tes classes (constructeur)
                                  • la possibilité de copier les données d'une instance existante pour créer une autre instance de ta classe (constructeur de copie)
                                  • la possibilité d'assigner les valeur d'une instance existante à  une autre instance existante (opérateur d'affectation)
                                  • la possibilité de détruire une instance existante (destructeur, pourrait sans doute garder son implémentation par défaut).

                                  (en gros, il s'agit de la forme canonique orthodoxe de Coplien :D )

                                  En plus de ces quatre invariants, nous souhaiterions voir apparaitre les services que ces classes exposent, car cela fait aussi partie des invariants de la classe.

                                  Et, dans l'idéal, nous voudrions voir comment tu implémente ces services, parce que certains d'entre eux (pour ne pas dire la plupart d'entre eux) risquent d'imposer des conditions qui devront être vérifiées au plus tard au moment où l'on fait appel à ces services car dans le cas contraire, il s'agit d'une erreur de programmation.

                                  Nous aimerions pouvoir nous assurer que ces conditions (que l'on appelle "pré conditions") soient bel et bien prises en compte ;)

                                  En gros, pour que tu comprenne la notion d'invariant, dis toi que c'est tout ce que ta classe est capable de faire "par elle-même" pour nous assurer à nous, pauvre humains trop souvent distraits:

                                  • que nous n'avons aucun besoin de savoir comment les données internes sont effectivement représentées
                                  • que nous pouvons manipuler une instance de la classe sans risquer de faire une connerie
                                  • que si nous transmettons des données cohérentes, pour n'importe quel service, nous obtiendrons un résultat cohérent
                                  • qu'elle nous obligera à revoir notre programmation si nous laissons passer des données "d'entrée" incohérente
                                  • qu'elle nous signalera les problèmes liés à des données sur lesquelles nous n'avons pas "tout le contrôle possible" (ex: le contenu d'un fichier, l'introduction de l'utilisateur, le résultat d'une requête effectuée auprès d'un serveur distant, ou, tout simplement une allocation de ressource qui échoue) et qui l'empêcherait de fournir un résultat cohérent, malgré des données "a priori"  cohérentes

                                  tbc11 a écrit:

                                  La construction du maillage triangulaire va s'appuyer sur les règles de Delaunay, qui s'appuient sur le cercle circonscrit au triangle que j'essaie de former. Par ailleurs, l'aire du triangle me sera utile plus tard.

                                  <snip>

                                  C'est très bien, mais ca, en tant qu'utilisateur de ta classe, on s'en fout royalement, des règles de création de ton maillage.

                                  Parce que c'est ce que l'on appelle un "détail d'implémentation": Nous ne doutons pas  que tu aies d'excellentes raisons d'utiliser les règles de Delaunay, mais:

                                  • Ce qui nous intéresse, c'est que le maillage soit créé au final
                                  • si, demain, tu trouve un algorithme qui s'avère plus efficace pour créer ton maillage, tu dois pouvoir l'utiliser, et cela ne changera rien pour  nous... 

                                  Dans le pire des cas, avec un même jeu de données, nous risquons de nous retrouver avec un maillage différent (ce qui pourrait, dans certaines situations, s'avérer un peu embêtant), mais si la différence de maillage te permet de traiter les différentes mailles de manière plus efficace, ce sera "tout bénef" pour l'utilisateur de ta classe ;)

                                  tbc11 a écrit:

                                  Pour chaque triangle, j'ai donc 7 attributs :

                                   Les attributs de ta classe on s'en fout, vu que ce sont également des "détails d'implémentation" et que nous ne devrions pas y  avoir accès

                                  Ou, du moins l'accès que l'on pourrait avoir à ces attributs devrait être particulièrement limité (à une question du genre "quelle est la valeur de telle information", si tant est que cela fasse sens de permettre à l'utilisateur de poser cette question).

                                  tbc11 a écrit:

                                  Les calculs pour établir les 4 réels double précision, à partir des 3 sommets, ne sont pas terrifiants, mais pas immédiats et coûtent de la CPU :

                                  • D'où l'intérêt de le faire dès la constitution du triangle et de les mémoriser dans la classe, afin d'éviter de les refaire à chaque fois qu'on interroge le triangle.
                                  • Oui mais qui dit mémorisation implique d'assurer en permanence la cohérence entre ces 4 réels double précision et les sommets déclarés. Sinon, l'algorithme (l'utilisateur de ma classe) va se planter complètement.

                                   Tout cela, c'est de la "popote interne": ton raisonnement semble bon, mais, en tant qu'utilisateur de ta classe, on se fout pas mal du raisonnement que tu auras suivi. En tant qu'utilisateur de ta classe, on attend qu'elle soit capable de nous rendre un certain nombre de services qui peuvent

                                  • soit être une question : quel est la valeur de XXX? tel (segment de) droite intersecte-t-il avec telle maille? ou autres
                                  • soit être un ordre : déplace tel point de 4 vers la gauche et de 10 vers le haut (*)

                                  Et, bien sur, que le maillage reste cohérent avec le résultat obtenu lorsque l'on a donné ce genre d'ordre.

                                  (*) pour autant bien sur qu'il y ait un sens à  permettre à l'utilisateur de donner un tel ordre ;)

                                  tbc11 a écrit:

                                  • La classe "Maille" aurait un sens

                                  Bien sur que la classe Maille a un sens, pas ** forcément ** parce qu'elle assure un invariant quelconque (que tu ne nous a pas encore présenté, d'ailleurs, bien qu'elle n'en soit pas exempte, je peux te l'assurer ;) ), mais simplement parce que tu as parlé de la notion de maille dans ton analyse des besoins.

                                  Il faut donc que tu puisse représenter cette notion d'une manière ou d'une autre dans ton code; tout comme il faut que tu puisse représenter la notion de point, la notion de noeud la notion de liste de points/ liste de noeuds et la notion de maillage dans ton code, parce que, d'une manière ou d'une autre, tu n'auras "pas d'autre choix" que de faire appel à  ces notions lorsque tu voudras décrire la logique à suivre pour fournir un service quelconque ;)

                                  • 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 septembre 2020 à 0:25:36

                                    Merci, mais j'aurais préféré obtenir des réponses plus concrètes à mes questions.

                                    Je reprécise ce que j'essaie de faire : une implémentation la plus efficace possible en C++ de l'algorithme de Delaunay de maillage triangulaire du plan sur la base de nœuds imposés.

                                    Si les aspects algorithmiques (la fameuse "popote interne" et autres "détails d'implémentation" ) vous intéressent, alors je vous invite à lire la littérature consacrée, par exemple :

                                    http://math.uni.lu/eml/projects/reports/MathExp_Palmirotta.pdf

                                    http://annabellecollin.perso.math.cnrs.fr/Mesh/ProjetGenerationMaillageDelaunay.pdf

                                    Si cela ne vous intéresse pas, alors pourriez-vous s'il vous plaît répondre plus directement à mes questions ?

                                    Que l'on se comprenne bien, en matière d'analyse numérique :

                                    • L'algorithme mathématique (par exemple, celui de Delaunay) dicte ce que doit faire in fine le programme, quel qu'en soit le langage de programmation.
                                    • Et pas l'inverse.

                                    Pour en revenir aux éléments de réponse qui m'ont été faits :

                                    koala01 a écrit: si, demain, tu trouve un algorithme qui s'avère plus efficace pour créer ton maillage, tu dois pouvoir l'utiliser, et cela ne changera rien pour  nous...

                                    Si demain quelqu'un trouvait un algorithme plus efficace que celui de Delaunay (1924), et bien le programme changera.

                                    Si c'est pour conclure que quel que soit l'algorithme, l'interpolation dans le plan consiste à donner (x,y) pour voir retournée la valeur interpolée de la fonction 2D à représenter en ce point, disons... que je n'ai pas besoin de ce forum pour le savoir.

                                    koala01 a écrit: On souhaiterait donc, à tout le moins, voir quatre invariants "de base" apparaitre, à savoir:

                                    • la possibilité de créer une instance de tes classes (constructeur)
                                    • la possibilité de copier les données d'une instance existante pour créer une autre instance de ta classe (constructeur de copie)
                                    • la possibilité d'assigner les valeur d'une instance existante à  une autre instance existante (opérateur d'affectation)
                                    • la possibilité de détruire une instance existante (destructeur, pourrait sans doute garder son implémentation par défaut).

                                    Sur le constructeur : je proposais précisément d'inclure les relations de dépendances (entre les 7 attributs) dès la construction de la maille. Je n'ai eu de réponse précise à ma proposition.

                                    Copier une maille : n'a aucun sens dans mon contexte applicatif. Ce n'est pas un besoin.

                                    Tester que 2 mailles sont égales : n'a aucun sens dans mon contexte applicatif. Ce n'est pas un besoin.

                                    Sur le destructeur : je tenais justement à détruire une maille proprement - y compris toutes les variables intermédiaires qu'elle comportait. Je n'ai pas eu non plus de réponse précise.

                                    Serait-il possible d'obtenir des réponses précises à mes questions, ou faut-il arrêter la discussion là ?

                                    • Partager sur Facebook
                                    • Partager sur Twitter
                                      14 septembre 2020 à 6:03:27

                                      tbc11 a écrit:

                                      Si demain quelqu'un trouvait un algorithme plus efficace que celui de Delaunay (1924), et bien le programme changera.

                                      Justement non, c'est là que tu te trompes lourdement... Tu dois organiser tes classes de manière à ce que leur utilisation reste parfaitement identique, quel que soit la manière dont tu organise les données ou les algorithmes utilisés.

                                      Prenons ta classe Point, par exemple.  Dans un premier temps, tu pourrais lui donner une forme proche de

                                      class Point{
                                      public:
                                          /* le constructeur par défaut: le point se trouve
                                           * en 0 pour l'abscisse
                                           * et 0 pour l'ordonnée
                                           * (j'utilise une fonctionnalité apparue en 2011 qui
                                           *  s'appelle "délégation du constructeur )
                                           */
                                          Point() : Point{0,0}{
                                          }
                                          /* un point avec un abscisse et une ordonnée bien précis
                                           * j'utilise la liste d'initialisation pour définir
                                           * les valeurs internes 
                                           */
                                          Point(int x, int y): x_{x}, y_{y}{
                                          }
                                          /* Il doit pouvoir répondre à deux questions essentielles
                                           * - Quelle est la valeur de l'abscisse?
                                           * - Quelle est la valeur de l'ordonnée?
                                           */
                                          int positionX() const{
                                              return x_;
                                          }
                                          int positionY() const{
                                              return y_;
                                          }
                                          /* Et il doit pouvoir obéir à un ordre bien particulier :
                                           * Déplace toi de <autant> en abscisse et de <autant>
                                           * en coordonnée
                                           * il doit en outre s'assurer que sa nouvelle position
                                           * reste valide
                                           */
                                          void move(int diffX, int diffY){
                                             /* l'abscisse ne peut pas être inférieur à  0 */
                                             assert(x_+diffX >= 0 && "abscisse invalide detecte!");
                                             /* l'ordonnée ne peut pas être inférieur à  0 */
                                             assert(y_+diffY >= 0 && "ordonnee invalide detectee!");
                                             /* si c'est bon, on déplace effectivement le point */
                                             x_ += diffX;
                                             y_ += diffY;
                                          }
                                      private:
                                          /* Les données interne: */
                                          int x_;
                                          int y_;
                                      };
                                      /* Accessoirement, parce que ce sont des services utiles
                                       * mais pas indispensables, je peux ajouter une fonction
                                       * libre qui fera malgré tout partie de l'interface
                                       * globale du point
                                       * Elle renvoie la distance euclidienne au carré entre
                                       * deux point (parce que la racine carré prend énormément
                                       * de temps à être calculée)
                                       */
                                      int squareDistance(Point const & a, Point const &b){
                                          int minX = std::min(a.positionX(), b.positionX());
                                          int maxX = std::max(a.positionX(), b.positionX());
                                          int minY = std::min(a.positionY(), b.positionY());
                                          int maxY = std::max(a.positionY(), b.positionY());
                                          return ((maxX - minX) * (maxX - minX))+
                                                 ((maxY - minY) * (maxY - minY));
                                      }

                                      Mettons que, demain, après mure réflexion, je décide de modifier la manière dont l'abscisse et l'ordonnée sont représentée.

                                      Cela ne doit absolument rien changerà la manière dont l'utilisateur pourra manipuler ma classe. Mieux encore: il y a de fortes chances pour que je ne doive même pas modifier l'implémentation de squareDistance!  La preuve:

                                      class Point{
                                      public:
                                          /* le constructeur par défaut: le point se trouve
                                           * en 0 pour l'abscisse
                                           * et 0 pour l'ordonnée
                                           * lui n'a aucune raison de changer
                                           */
                                          Point() : Point{0,0}{
                                          }
                                          /* un point avec un abscisse et une ordonnée bien précis
                                           */
                                          Point(int x, int y): {
                                              coordinates_[0] = x;
                                              coordinates_[1] = y;
                                          }
                                          /* Il doit pouvoir répondre à deux questions essentielles
                                           */
                                          int positionX() const{
                                              return coordinates_[0];
                                          }
                                          int positionY() const{
                                              return coordinates_[1];
                                          }
                                          /* Et il doit pouvoir obéir à un ordre bien particulier :
                                           * Déplace toi de <autant> en abscisse et de <autant>
                                           * en coordonnée
                                           * il doit en outre s'assurer que sa nouvelle position
                                           * reste valide
                                           */
                                          void move(int diffX, int diffY){
                                             /* l'abscisse ne peut pas être inférieur à  0 */
                                             assert(coordinates_[0]+diffX >= 0 && "abscisse invalide detecte!");
                                             /* l'ordonnée ne peut pas être inférieur à  0 */
                                             assert(coordinates_[1]+diffY >= 0 && "ordonnee invalide detectee!");
                                             /* si c'est bon, on déplace effectivement le point */
                                             coordinates_[0] += diffX;
                                             coordinates_[1] += diffY;
                                          }
                                      private:
                                          /* Les données interne: 
                                           * j'utilise std::array, apparu en 2011 
                                           */
                                          std::array<int, 2> coordinates_;
                                      };
                                      /* La fonction squareDistance reste inchangée */

                                      Si, dans une semaine, tu découvre la bibliothèque gml et que tu décides de l'utiliser, cela ne changera toujours absolument rien à la manière d'utiliser ta classe! voici à  quoi elle ressemblerait

                                       class Point{
                                      public:
                                          /* le constructeur par défaut: le point se trouve
                                           * en 0 pour l'abscisse
                                           * et 0 pour l'ordonnée
                                           * lui n'a aucune raison de changer
                                           */
                                          Point() : Point{0,0}{
                                          }
                                          /* un point avec un abscisse et une ordonnée bien précis
                                           */
                                          Point(int x, int y): coordinates_{x,y}{
                                              
                                          }
                                          /* Il doit pouvoir répondre à deux questions essentielles
                                           */
                                          int positionX() const{
                                              return coordinates_.x;
                                          }
                                          int positionY() const{
                                              return coordinates_.y;
                                          }
                                          /* Et il doit pouvoir obéir à un ordre bien particulier :
                                           * Déplace toi de &lt;autant&gt; en abscisse et de &lt;autant&gt;
                                           * en coordonnée
                                           * il doit en outre s'assurer que sa nouvelle position
                                           * reste valide
                                           */
                                          void move(int diffX, int diffY){
                                             /* l'abscisse ne peut pas être inférieur à  0 */
                                             assert(coordinates_.x+diffX >= 0 &amp;&amp; "abscisse invalide detecte!");
                                             /* l'ordonnée ne peut pas être inférieur à  0 */
                                             assert(coordinates_.y+diffY >= 0 &amp;&amp; "ordonnee invalide detectee!");
                                             /* si c'est bon, on déplace effectivement le point */
                                             coordinates_.x += diffX;
                                             coordinates_.y += diffY;
                                          }
                                      private:
                                          /* Les données interne: 
                                           */
                                          glm::ivec2 coordinates_;
                                      };
                                      /* la fonction squareDistance reste encore une fois identique
                                       */

                                      Voilà trois exemples d'implémentation d'une classe point qui fonctionneront parfaitement et de la même manière (il faudra juste penser à inclure le fichier <array> pour le deuxième et à inclure le fichier <glm/glm.hpp> pour le troisième) et qui représente exactement tout ce que j'essaye de t'expliquer maintenant depuis plusieurs jours:

                                      Si tu tiens à  encapsuler tes données, tu dois le faire suffisamment bien pour que, quelle que soit l'organisation interne de tes donnée, quel que soit les algorithmes que tu utilise, l'utilisateur de ta classe (toi, dés le moment où tu ne travaille plus sur le code de la classe)

                                      • ne puisse pas faire de connerie en la créant ou en la manipulant
                                      • puisse écrire un code qu'il ne devra pas changer tous le quinze jours parce que tu as trouvé "la super bibliothèque qui fait ca mieux"

                                      Et ce qui permet ces deux choses est simple: il faut  une interface aussi limitée que possible: j'ai deux constructeurs (indispensables, malheureusement) et trois fonctions membres (plus une fonction libre qui vient "en suppélement").  ET C'EST TOUT!

                                      tbc11 a écrit:

                                      Sur le constructeur : je proposais précisément d'inclure les relations de dépendances (entre les 7 attributs) dès la construction de la maille. Je n'ai eu de réponse précise à ma proposition.

                                      Ne fais pas simplement des propositions! Montre nous le code que tu t'apprêtes à mettre en oeuvre, exactement tel que tu l'as écrit, que nous puissions le copier, le compiler et voir s'il fonctionne correctement!

                                      Dis toi bien que, tant que l'on n'a pas quelque chose de concret sur lequel travailler, nous ne pouvons que te donner notre point de vue sur la manière de t'y prendre

                                      Et si tu ne nous montre pas ton code, nous on ne peut pas deviner ce qui te pose problème ;)

                                      Par contre, vu que tu insistes sur certains points conceptuels en laissant entrevoir une mauvaise compréhension de ta part, et malgré le fait que tu dise "Sujet clos sur ce point.", hé bien non, le sujet n'est pas forcément clos.

                                      Parce que  si c'est pour te laisser avec ta mauvaise compréhension d'un principe ou d'un problème, le forum ne sert à rien.

                                      Tu as la chance de recevoir des avis de professionnels dont certains ont plus de quinze ans d'expérience. Pourquoi ne pas en profiter pour améliorer ta compréhension des concepts et des principes sous jacents? Pour te rendre tout doucement compte que l'écriture d'un code, quel que soit le langage, ne devrait être  que la dernière étape d'un processus beaucoup plus long de conception, et qu'elle se limite généralement à un long et fastidieux travaille de dactylographie et de traduction?

                                      tbc11 a écrit:

                                      Copier une maille : n'a aucun sens dans mon contexte applicatif. Ce n'est pas un besoin.

                                      Si tu le penses réellement, cela me va très bien.  Mais alors il faut lui donner une sémantique d'entité, pour être sur qu'elle ne sera jamais copiée suite à l'oubli d'une simple esperluette dans le prototype d'une fonction, par exemple.

                                      Faut pas croire, tu sais? contrairement aux apparences, je suis un gars pour le moins conscilient ;)

                                      Dés le moment où les principes généraux sont respectés, moi, je n'ai rien à dire :D

                                      tbc11 a écrit:

                                      Sur le destructeur : je tenais justement à détruire une maille proprement - y compris toutes les variables intermédiaires qu'elle comportait. Je n'ai pas eu non plus de réponse précise.

                                      Ben, en fait, tu n'as pas eu de réponse parce que, pour ce que l'on en voit, la destruction de ta maille ne nécessite aucune intervention de ta part :p

                                      Comme toutes les données membres de ta maille sont créées sur la pile (sans avoir recours à  l'allocation dynamique de la mémoire), elles seront toutes détruites automatiquement, dans l'ordre inverse de leur déclaration, au moment où l'instance de maille sera détruite.

                                      C'est chouette, hein, le RAII ??? :D

                                      -
                                      Edité par koala01 14 septembre 2020 à 6:06:45

                                      • 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 septembre 2020 à 15:39:04

                                        OK - merci beaucoup pour ces explications.

                                        Je n'ai pas pour l'instant de code à montrer, précisément parce que je voulais définir la manière de structurer mes variables et objets, avant de coder. Du coup, cela devient compliqué pour m'expliquer : comment décrire quelque chose qui n'existe pas encore ?

                                        Je vais procéder comme je pense (peut-être mal à propos) qu'il est approprié de faire, compte-tenu de mon contexte applicatif, et reviendrai vers vous dès que j'aurai quelque chose de plus concret à montrer.

                                        1. Une question technique dans le code que tu me montres, à propos des accesseurs :

                                        class Point{
                                        public:
                                            /* le constructeur par défaut: le point se trouve
                                             * en 0 pour l'abscisse
                                             * et 0 pour l'ordonnée
                                             * lui n'a aucune raison de changer
                                             */
                                            Point() : Point{0,0}{
                                            }
                                            /* un point avec un abscisse et une ordonnée bien précis
                                             */
                                            Point(int x, int y): coordinates_{x,y}{
                                                 
                                            }
                                            /* Il doit pouvoir répondre à deux questions essentielles
                                             */
                                            int positionX() const{
                                                return coordinates_.x;
                                            }
                                        etc...
                                        };

                                        L'accesseur positionX() const n'est pas seulement déclaré dans ta déclaration de classe, mais il y est même défini : le code de la fonction est donné. Est-ce que cela suffit à ce qu'il soit inline ? Et est-ce pour cela que tu n'as pas fait usage du mot clef inline ?

                                        2. Retour sur ce que mon code va faire - et qui me semble important pour la structuration des variables - ce que je n'ai peu-être pas assez clairement expliqué.

                                        Indépendamment de toute représentation des variables et même de tout langage de programmation, mon problème est le suivant :

                                        - Je cherche à interpoler une fonction 2D - de type f (x, y) - dans le plan

                                        - Sachant que je connais sa valeur en certains points (ce que j'appelle les noeuds) : je connais des triplets (xi, yi, fi) donnés - et qui resteront inchangés durant toute l'exécution du programme.

                                        - Il y a deux phases dans le processus :

                                           1) Construire un maillage triangulaire du plan à partir de ces noeuds : il y a une multiplicité de maillages triangulaires différents possibles, mais il y en n'a qu'un seul qui se prête à l'interpolation - celui de Delaunay.

                                          2) Exploiter ce maillage. Une fois ce maillage défini, on le fige. L'utilisateur n'aura qu'à rentrer les points sur lesquels il désire faire l'interpolation : ces points sont quelconques, et en nombre quelconque (en fonction de ce que l'utilisateur souhaite faire) et sont a priori différents des noeuds du maillage - par nature. Sinon, il n'y a pas besoin d'interpoler !

                                        C'est la phase de construction du maillage qui est délicate - surtout que c'est un processus incrémental. Je vais te l'illustrer pas à pas :

                                        Etape 1 :  le maillage est vide. Je prends 3 noeuds au hasard parmi les n noeuds : par exemple les noeuds n°17, 23 et 8. Et je forme un triangle T1 avec ça.

                                        A ce stade, mon maillage n'est constitué que de la maille T1 formée des sommets (17, 23, 8)

                                        Etape 2 : je pêche un quatrième noeud - évidemment distinct des 3 précédents. Par exemple, le noeud n°45

                                        3 cas de figure peuvent se présenter :

                                        Cas n°1

                                        Si ce noeud n°45 est assez éloigné du triangle T1 ("assez éloigné" au sens d'une métrique parfaitement définie qu'une fonction sera chargée d'évaluer), alors on va constituer une deuxième maille T2 formée des noeuds (8, 23, 45)

                                        A ce stade, le maillage est constitué de 2 mailles : T1 (17, 23, 8) et T2 (23, 8 ,45)

                                        Cas n°2

                                        Mais si ce noeud 45 est trop proche de la maille T1 (toujours au sens d'une métrique parfaitement définie), alors les critères de Delaunay m'interdisent de faire ça, parce que T2 est un "mauvais triangle" : trop aplati donc mauvais pour l'interpolation.

                                        Il va falloir faire cela :

                                        C'est à dire :

                                        a/ Détruire les mailles T1 et T2

                                        b/ Recréer les mailles T'1 (17, 45, 8) et T'2 (17, 45, 23)

                                        Cas n°3

                                        Si jamais le noeud n°45 tombait à l'intérieur de la maille T1 : ça peut arriver, vu qu'on le pêche au hasard parmi la liste des noeuds :

                                        Alors il faut :

                                        a) Détruire la  maille T1

                                        b) Et en recréer 3 autres mailles :T'1 (17, 45,8), T'2 (17, 45, 23) et T'3 (45, 8, 23)

                                        ... et ainsi de suite jusqu'à avoir introduit tous les noeuds de la liste connue au départ.

                                        Ce qui est important :

                                        • A aucun moment, les coordonnées des noeuds n'ont varié
                                        • Ce qui a change est la manière de les associer entre eux pour former des triangles.

                                        C'est pour cette raison, que ce qui est constitutif d'une maille sont les 3 indices des 3 noeuds formant les sommets et non nécessairement les coordonnées (x,y) des ces derniers.La donnée de ces 3 entiers (3*4 = 12 octets) est suffisante pour définir sans ambigüité la maille. C'est bien moins que 9 réels double précision (3 abscisses, 3 ordonnées, 3 valeurs de la fonction à interpoler => 9*8 = 72 octets).

                                        Ces coordonnées doivent néanmoins être connues (pour faire mes calculs) - mais les indices des noeuds concernés me suffisent à les retrouver.

                                        Est-ce maintenant plus clair pour toi ?

                                        -
                                        Edité par tbc11 14 septembre 2020 à 18:02:20

                                        • Partager sur Facebook
                                        • Partager sur Twitter
                                          14 septembre 2020 à 18:31:35

                                          Passer par des indices demande des indirections.

                                          Il y a probablement des compromis espace/calcul à faire pour l'optimisation, mais c'est trop tôt.

                                          Donc, faites simples.

                                          -
                                          Edité par bacelar 14 septembre 2020 à 18:41:07

                                          • Partager sur Facebook
                                          • Partager sur Twitter
                                          Je recherche un CDI/CDD/mission freelance comme Architecte Logiciel/ Expert Technique sur technologies Microsoft.
                                            14 septembre 2020 à 18:36:58

                                            et puis

                                            > Nouveau en C++, je veux m'exercer sur la base d'un cas concret qui m'intéresse

                                            il s'agit d'un exercice pour apprendre à concevoir  un programme à base de classes et à le programmer à C++.

                                            Donc pas d'objectifs de performances à ce stade, et aucune considération sur le volume de données qui justifie qu'on s'affole sur la micro-optimisation des performances. Si ça devait traiter des maillages avec des milliards de points, ça serait le cas, mais là....


                                            • Partager sur Facebook
                                            • Partager sur Twitter
                                              14 septembre 2020 à 21:34:21

                                              tbc11 a écrit:

                                              OK - merci beaucoup pour ces explications.

                                              Je n'ai pas pour l'instant de code à montrer, précisément parce que je voulais définir la manière de structurer mes variables et objets, avant de coder. Du coup, cela devient compliqué pour m'expliquer : comment décrire quelque chose qui n'existe pas encore ?

                                              Je vais procéder comme je pense (peut-être mal à propos) qu'il est approprié de faire, compte-tenu de mon contexte applicatif, et reviendrai vers vous dès que j'aurai quelque chose de plus concret à montrer.

                                              1. Une question technique dans le code que tu me montres, à propos des accesseurs :

                                              class Point{
                                              public:
                                                  /* le constructeur par défaut: le point se trouve
                                                   * en 0 pour l'abscisse
                                                   * et 0 pour l'ordonnée
                                                   * lui n'a aucune raison de changer
                                                   */
                                                  Point() : Point{0,0}{
                                                  }
                                                  /* un point avec un abscisse et une ordonnée bien précis
                                                   */
                                                  Point(int x, int y): coordinates_{x,y}{
                                                       
                                                  }
                                                  /* Il doit pouvoir répondre à deux questions essentielles
                                                   */
                                                  int positionX() const{
                                                      return coordinates_.x;
                                                  }
                                              etc...
                                              };

                                              L'accesseur positionX() const n'est pas seulement déclaré dans ta déclaration de classe, mais il y est même défini : le code de la fonction est donné. Est-ce que cela suffit à ce qu'il soit inline ? Et est-ce pour cela que tu n'as pas fait usage du mot clef inline ?

                                              2. Retour sur ce que mon code va faire - et qui me semble important pour la structuration des variables - ce que je n'ai peu-être pas assez clairement expliqué.

                                              Indépendamment de toute représentation des variables et même de tout langage de programmation, mon problème est le suivant :

                                              - Je cherche à interpoler une fonction 2D - de type f (x, y) - dans le plan

                                              - Sachant que je connais sa valeur en certains points (ce que j'appelle les noeuds) : je connais des triplets (xi, yi, fi) donnés - et qui resteront inchangés durant toute l'exécution du programme.

                                              - Il y a deux phases dans le processus :

                                                 1) Construire un maillage triangulaire du plan à partir de ces noeuds : il y a une multiplicité de maillages triangulaires différents possibles, mais il y en n'a qu'un seul qui se prête à l'interpolation - celui de Delaunay.

                                                2) Exploiter ce maillage. Une fois ce maillage défini, on le fige. L'utilisateur n'aura qu'à rentrer les points sur lesquels il désire faire l'interpolation : ces points sont quelconques, et en nombre quelconque (en fonction de ce que l'utilisateur souhaite faire) et sont a priori différents des noeuds du maillage - par nature. Sinon, il n'y a pas besoin d'interpoler !

                                              C'est la phase de construction du maillage qui est délicate - surtout que c'est un processus incrémental. Je vais te l'illustrer pas à pas :

                                              Etape 1 :  le maillage est vide. Je prends 3 noeuds au hasard parmi les n noeuds : par exemple les noeuds n°17, 23 et 8. Et je forme un triangle T1 avec ça.

                                              A ce stade, mon maillage n'est constitué que de la maille T1 formée des sommets (17, 23, 8)

                                              Etape 2 : je pêche un quatrième noeud - évidemment distinct des 3 précédents. Par exemple, le noeud n°45

                                              3 cas de figure peuvent se présenter :

                                              Cas n°1

                                              Si ce noeud n°45 est assez éloigné du triangle T1 ("assez éloigné" au sens d'une métrique parfaitement définie qu'une fonction sera chargée d'évaluer), alors on va constituer une deuxième maille T2 formée des noeuds (8, 23, 45)

                                              A ce stade, le maillage est constitué de 2 mailles : T1 (17, 23, 8) et T2 (23, 8 ,45)

                                              Cas n°2

                                              Mais si ce noeud 45 est trop proche de la maille T1 (toujours au sens d'une métrique parfaitement définie), alors les critères de Delaunay m'interdisent de faire ça, parce que T2 est un "mauvais triangle" : trop aplati donc mauvais pour l'interpolation.

                                              Il va falloir faire cela :

                                              C'est à dire :

                                              a/ Détruire les mailles T1 et T2

                                              b/ Recréer les mailles T'1 (17, 45, 8) et T'2 (17, 45, 23)

                                              Cas n°3

                                              Si jamais le noeud n°45 tombait à l'intérieur de la maille T1 : ça peut arriver, vu qu'on le pêche au hasard parmi la liste des noeuds :

                                              Alors il faut :

                                              a) Détruire la  maille T1

                                              b) Et en recréer 3 autres mailles :T'1 (17, 45,8), T'2 (17, 45, 23) et T'3 (45, 8, 23)

                                              ... et ainsi de suite jusqu'à avoir introduit tous les noeuds de la liste connue au départ.

                                              Ce qui est important :

                                              • A aucun moment, les coordonnées des noeuds n'ont varié
                                              • Ce qui a change est la manière de les associer entre eux pour former des triangles.

                                              C'est pour cette raison, que ce qui est constitutif d'une maille sont les 3 indices des 3 noeuds formant les sommets et non nécessairement les coordonnées (x,y) des ces derniers.La donnée de ces 3 entiers (3*4 = 12 octets) est suffisante pour définir sans ambigüité la maille. C'est bien moins que 9 réels double précision (3 abscisses, 3 ordonnées, 3 valeurs de la fonction à interpoler => 9*8 = 72 octets).

                                              Ces coordonnées doivent néanmoins être connues (pour faire mes calculs) - mais les indices des noeuds concernés me suffisent à les retrouver.

                                              Est-ce maintenant plus clair pour toi ?

                                              -
                                              Edité par tbc11 il y a 19 minutes

                                              tbc11 a écrit:

                                              OK - merci beaucoup pour ces explications.

                                              Je n'ai pas pour l'instant de code à montrer, précisément parce que je voulais définir la manière de structurer mes variables et objets, avant de coder. Du coup, cela devient compliqué pour m'expliquer : comment décrire quelque chose qui n'existe pas encore ?

                                              Ben, comme disait l'autre (Nicolas Boileau, pour être précis)

                                              Ce qui se conçoit bien s'énonce clairement

                                              Et les mot pour le dire viennent aisément

                                              Il est vrai que c'est presqu'un art que d'exprimer simplement ses besoins, ses attentes, ses convictions.

                                              Cependant, lorsque l'on s'arrête quelques instants pour concevoir clairement ce que l'on veut exprimer, cela devient tout de suite plus facile ;)

                                              Cependant, si  je peux me permettre un conseil: n'attend pas d'avoir "la solution parfaite" avant de commencer à  écrire ton code.  Autrement, tu passera ton temps à repousser l'écriture du code parce que tu crois avoir trouvé "une meilleure solution", quelle qu'en soit l'origine, quelle qu'en soit la forme.  Et au final, ton besoin ne sera jamais remplis.

                                              Par contre, le fait d'écrire le code à partir de la solution "qui te semble apropirée" (à un moment donné) te permettra:

                                              • D'exprimer ta pensée sous une forme utilisable
                                              • de réfléchir à partir d'une base concrète
                                              • et  -- potentiellement -- de mettre en évidence certains problèmes auxquels tu n'avais pas forcément pensé, dont tu t'étais dit "il faudra que je règle cela aussi, mais je le ferai plus tard" (or, "plus tard", ca veut dire "jamais"), ou même qui s'avèrent au final beaucoup plus grave, beaucoup plus importants que ce que tu pensais au départ.

                                              Et puis, personne n'a jamais dit que ton code devait être "sculpté dans le marbre".  Bien au contraire. Le code est quelque chose de presque "vivant", qui évolue au rythme de tes idées et selon ta compréhension générale du domaine ou des besoins que tu souhaites remplir:

                                              Si tu estimes que dix, vingt, cinquante ... deux cents lignes de code pourraient être réécrites "autrement", mais que cette réécriture correspond d'avantage à ta compréhension du problème à l'heure actuelle et, surtout, qu'elle rendra le code "meilleur", alors il n'y a pas à hésiter: on sélectionne les lignes en question, on les efface, et on recommence. 

                                              L'idéal étant "dans la mesure du possible" de garder une interface "la plus stable possible", mais, si ton changement de point de vue  te fait te rendre compte que certaines fonctions sont inutiles ou qu'elles ne correspondent plus aux services que l'on est en droit d'attendre de la part de l'application, hé bien, il faut les supprimer "sans remords" ;)

                                              (note: un gestionnaire de versions concurrentes comme git, par exemple, te permettront de garder "plus facilement" l'historique, au cas où tu changerait d'avis ;) )

                                              tbc11 a écrit:

                                              Je vais procéder comme je pense (peut-être mal à propos) qu'il est approprié de faire, compte-tenu de mon contexte applicatif, et reviendrai vers vous dès que j'aurai quelque chose de plus concret à montrer.

                                              Voilà une excellente idée. Au moins, même si ce n'est pas parfait (et il ne sert sans doute à rien de se faire des illusions, rien n'est jamais parfait lors du "premier jet"), cela nous donera à tous une base de travail à partir de laquelle nous pourrons t'indiquer ce qui est (très) bien (car il y aura forcément du bien), ce qui est "perfectible" et ce qui doit impérativement être modifié / corrigé (car il y en aura aussi ;) )

                                              tbc11 a écrit:

                                              L'accesseur positionX() const n'est pas seulement déclaré dans ta déclaration de classe, mais il y est même défini : le code de la fonction est donné.

                                              Oui, c'est effectivement ce que l'on entend généralement par les termes "définir une fonction": que le code qui implémente(encore un autre terme pour dire la même chose cette fonction est fourni ;) :D

                                              tbc11 a écrit:

                                               Est-ce que cela suffit à ce qu'il soit inline ? Et est-ce pour cela que tu n'as pas fait usage du mot clef inline ?

                                              Hé bien oui, en fait ... Car il existe bel et bien une règle qui dit que (traduction potentiellement à revoir)

                                              Toute fonction membre non virtuelle d'une classe ou d'une structure définie à l'intérieur de la définition de la classe (ou de la structure) est implicitement réputée inline

                                              A ceci près que ce n'est pas ma raison principale, surtout quand j'écris "à la volée" des morceaux de code qui sont principalement destinés à servir d'exemples ;)

                                              Pour être tout à fait honnête, l'inlining des fonctions est très loin d'être ma préoccupation principale. Mes raisons profondes, celles que je n'aime pas avouer et qui me poussent pourtant à écrire le code sous cette forme sont

                                              1. Parce que c'est autorisé par le langage: Aux erreurs de typographie et aux fichiers d'en-tête requis près, devrait normalement compiler sans aucun problème
                                              2. parce que cela fait moins de code à écrire et que je suis de nature fainéante
                                              3. Parce que cela permet de rendre le code "plus compact" et donc plus facile à lire
                                              4. Parce que cela permet -- à mon sens et même si je peux me tromper -- à celui qui le lira de se faire une meilleure idée d'ensemble du code que je lui présente

                                              Et oui, les raisons sont classées par ordre de préférence respective ;)

                                              tbc11 a écrit:

                                              - Sachant que je connais sa valeur en certains points (ce que j'appelle les noeuds) : je connais des triplets (xi, yi, fi) donnés - et qui resteront inchangés durant toute l'exécution du programme.

                                              Justement non!

                                              Tu connais les points, tu connais -- a priori -- les noeuds (le valeurs associées à chaque points), mais tu ne connais pas les triplets.  Les triplets, c'est justement ce que tu dois découvrir.

                                              Car, si tu connaissais les triplets, tu aurais déjà des mailles, et l'interpolation que tu nous décris par la suite n'aurait plus aucun sens, car plus aucune utilité!

                                              A moins que j'ai loupé quelque chose (ce qui reste toujours possible :p )?

                                              tbc11 a écrit:

                                                 1) Construire un maillage triangulaire du plan à partir de ces noeuds : il y a une multiplicité de maillages triangulaires différents possibles, mais il y en n'a qu'un seul qui se prête à l'interpolation - celui de Delaunay.

                                              En es tu tout à fait sur?

                                              Attention, je ne prétends pas que tu as tort! Je te demande juste si cette affirmation faite avec tellement de panache est une thèse confirmée et validée ou une hypothèse de travail.

                                              Car, a priori, s'il suffit simplement de dresser un maillage entre les différents noeuds, je dois au moins pouvoir te trouver trois solutions qui créeront un maillage cohérent et qui n'auront rien à voir avec Delaunay :p

                                              Cependant, n'ayant pas essayé d'implémenter ces solutions, je ne donne aucune garantie quant au fait que les maillages se prèteront à l'interpolation. Et c'est pour cela que je te demande si on part sur un thèse confirmée ou sur une hypothèse de travail.

                                              Car, vois tu, je suis un type simple, qui aime les choses simples (du genre d'un bon café ou d'une bonne bierre d'Abaye bue dans son jardin confortablement installé à regarder pousser ses tomates, surtout par le temps qu'il fait aujourd'hui :p )

                                              Et, surtout, je suis le genre de type qui n'aime pas ajouter plus de complexité que nécessaire dans son code.  Or, j'ai l'impression que Delaunoy est excessivement complexe, alors que je suis convaincu qu'il y a moyen de dresser un maillage à l'aide d'une fonction qui doit faire quoi? maximum vingt lignes de code?

                                              Du coup, si tu devais me dire que tu pars de l'hypothèse de travail que seul Delaunoy te permet d'obtenir ce que tu recherche, je te conseillerais de t'orienter vers la solution la plus simple dans un premier temps, quitte à "passer" à  la solution Delaunoy si le résultat ne correspond pas à tes attentes ;)

                                              tbc11 a écrit:

                                              Etape 1 :  le maillage est vide. Je prends 3 noeuds au hasard parmi les n noeuds : par exemple les noeuds n°17, 23 et 8. Et je forme un triangle T1avec ça.

                                              A ce stade, mon maillage n'est constitué que de la maille T1 formée des sommets (17, 23, 8)

                                              D'accord, après tout, je ne suis pas là pour te contrarier... Cependant, je voudrais être sur que l'on est sur la même longueur d'onde, et, pour cela, j'ai bien envie de te poser trois questions "qui fachent":

                                              • Pourquoi ces trois point là précisément? ou, si tu préfère, pourquoi faire intervenir le hasard dans ce choix?
                                              • Pourquoi dans cet ordre particulier? ou, si tu préfères, ne pourrait on pas envisager de les mettre dans leur ordre logique (8,17,23 dans le cas présent)?
                                              • Et surtout: quelle garantie y a-t-il qu'il n'y ait aucun point dans le triangle ainsi formé?  Est-ce que cela a seulement une quelconque importance?

                                              tbc11 a écrit:

                                              Cas n°2

                                              Mais si ce noeud 45 est trop proche de la maille T1 (toujours au sens d'une métrique parfaitement définie), alors les critères de Delaunay m'interdisent de faire ça, parce que T2 est un "mauvais triangle" : trop aplati donc mauvais pour l'interpolation.

                                              Il va falloir faire cela :

                                              C'est à dire :

                                              a/ Détruire les mailles T1 et T2

                                              b/ Recréer les mailles T'1 (17, 45, 8) et T'2 (17, 45, 23)

                                              D'accord, pour moi, ca me va...

                                              Euuhhh, ... Quelle est cette métrique parfaitement définie dont tu parle?

                                              tbc11 a écrit:

                                              Cas n°3

                                              Si jamais le noeud n°45 tombait à l'intérieur de la maille T1 : ça peut arriver, vu qu'on le pêche au hasard parmi la liste des noeuds :

                                              Alors il faut :

                                              a) Détruire la  maille T1

                                              b) Et en recréer 3 autres mailles :T'1 (17, 45,8), T'2 (17, 45, 23) et T'3 (45, 8, 23)

                                              ... et ainsi de suite jusqu'à avoir introduit tous les noeuds de la liste connue au départ.

                                              Ah, ben voilà  la réponse de ma question de là tout de suite ;) :D

                                              Et bon, jusque là, ca me va...

                                              Cependant, je dois t'avouer que ton T2 ne me semble pas vraiment beaucoup plus grand que le T2 que tu montre dans le cas2 et que tu te sens obligé de détruire pour former les mailles autrement :p

                                              En outre, tout cela me semble bien compliqué pour générer le maillage...

                                              Car je reste convaincu qu'il ne me faudra sans doute guère plus d'une vingtaine de ligne de code pour te créer un maillage sommes toutes équivalent, et très certainement plus rapidement.

                                              Mieux, je suis convaincu  qu'en rajoutant cette métrique dont tu parles (et dont je ne sais rien), le code pourrait rester beaucoup plus simple et efficace malgré tout ;)

                                              tbc11 a écrit:

                                              • A aucun moment, les coordonnées des noeuds n'ont varié

                                              Voilà au moins une information qui peut éviter pas mal de problèmes :D

                                              tbc11 a écrit:

                                              • Ce qui a change est la manière de les associer entre eux pour former des triangles.

                                              Je ne crois pas me tromper en disant que ca, c'était bien clair dans l'esprit de tous ;)

                                              tbc11 a écrit:

                                              C'est pour cette raison, que ce qui est constitutif d'une maille sont les 3 indices des 3 noeuds formant les sommets et non nécessairement les coordonnées (x,y) des ces derniers.La donnée de ces 3 entiers (3*4 = 12 octets) est suffisante pour définir sans ambigüité la maille. C'est bien moins que 9 réels double précision (3 abscisses, 3 ordonnées, 3 valeurs de la fonction à interpoler => 9*8 = 72 octets).

                                              J'ai bien conscience de la différence, merci quand même :D

                                              Cependant, en quoi cela vous poserait-il un problème de vous balader avec soixante octets de plus?

                                              A moins que tu ne veuille créer des millions de points et des dizaines de millions de mailles?  Et quand bien même? cela forecerait ton application à utiliser quoi? 60Mb de mémoire en plus? sur quoi? 8 Gb, sans doute? une paille :D 

                                              Surtout si cette paille permet de simplifier la logique de tout le reste, et, accessoirement, de fournir des résultats plus rapidement, même sans chercher, pour l'instant à optimiser les choses à tout prix ;)

                                              tbc11 a écrit:

                                              Ces coordonnées doivent néanmoins être connues (pour faire mes calculs) - mais les indices des noeuds concernés me suffisent à les retrouver.

                                              Le mais n'a aucun intérêt ici. Arrête toi à cette seule phrase "les coordonnées doivent être connues (et accessibles) pour faire mes calculs."

                                              J'en reviens à  la simplicité: si tu as besoin des coordonnées pour tes calculs alors que tu observe une maille, le plus simple reste encore de fournir ces coordonnées à la maille, surtout si les coordonnées ne risquent pas de changer ;)

                                              Quel intérêt y a-t-il à aller chercher la complexité en utilisant l'indice du point utilisé dans le tableau grâce auquel nous pourront récupérer la coordonnée?

                                              -
                                              Edité par koala01 14 septembre 2020 à 21:35:00

                                              • 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 septembre 2020 à 22:58:05

                                                Les mailles (triangulaires) regroupent de sommets (3), mais ces sommets figurent dans plusieurs mailles.

                                                Ce qui suggère que les mailles ne _contiennent_ pas les informations liées aux sommets, mais _fassent référence_ à des sommets. Chaque entité "sommet" possédant ses coordonnées comme attributs.

                                                Résumé : dans un graphe il y a

                                                • des sommets (qui ont des coordonnées),
                                                • des mailles,
                                                • et une association  n-m entre sommets et mailles.

                                                -
                                                Edité par michelbillaud 15 septembre 2020 à 12:04:50

                                                • Partager sur Facebook
                                                • Partager sur Twitter
                                                  15 septembre 2020 à 11:13:10

                                                  A vrai dire, j'ai pensé par la suite qu'on pouvait, au pire, utiliser un pointeur, ou mieux, un std::reference_wrapper pour, justement, permettre aux mailles de faire référence aux noeuds sans les contenir ;)

                                                  Après tout: nous devons dresser une liste de points et une liste de noeuds avant de dresser le maillage.

                                                  Dés lors, qu'est ce qui nous empêche d'avoir une maille proche de

                                                  class Maille{
                                                  public:
                                                  Maille(Node & n1, Node & n2, Node & n3):
                                                      noeud1_{n1},noeud2_{n2},noeud3_{n3}{
                                                  }
                                                  
                                                  double evaluate(std::function<double(Node const &,
                                                                                       Node const &,
                                                                                       Node const &)> foo){
                                                      return foo(noeud1_.get(), noeud2_.get(), noeud3_.get());
                                                  }
                                                  private:
                                                      std::reference_wrapper<Node> noeud1_;
                                                      std::reference_wrapper<Node> noeud2_;
                                                      std::reference_wrapper<Node> noeud3_;
                                                  };

                                                  A l'heure actuelle, je ne vois pas ce que la maille devrait faire d'autre ;)

                                                  NOTE: la fonction évaluate prend une std::function, comme paramètre (qui prend trois noeuds comme paramètres et renvoie un double) qui permet de représenter n'importe quel comportement spécifique à l'évaluation / l'interpolation de la maille en elle-même, et, plus précisément, des noeuds auxquels elle fait référence.

                                                  Ce pourrait, par exemple, être une fonction calcule la somme des valeurs de chaque noeuds, sous une forme proche de

                                                  /* renvoie la somme des valeurs de tous les noeuds */
                                                  double summ(Node const & n1, Node const &n2, Node const &n3){
                                                      return n1.value + n2.value + n3.value;
                                                  }

                                                  ou qui pourrait calculer la somme des distances euclidiennes entre les différents points qui composent la maille (si cela a du sens de le faire ;) sous une forme proche de

                                                  /* j'avais fourni la fonction de calcul des distances
                                                   * euclidiennes au carré plus haut, je la réintroduit
                                                   * ici pour la facilité
                                                   */
                                                  double squaredDistance(Point const & p1, Point const & p2){
                                                      double minX = std::min(p1.x, p2.x);
                                                      double maxX = std::max(p1.x, p2.x);
                                                      double minY = std::min(p1.y, p2.y);
                                                      double maxY = std::max(p1.y, p2.y);
                                                      return (maxX - minX)*(maxX - minX) +
                                                             (maxY - minY)*(maxY - minY);
                                                     
                                                  }
                                                  /* la fonction que je voulais montrer */
                                                  double summOfDistances(Node const & n1, Node const &n2, Node const & n3){
                                                      /* 3 noeuds, 3 distances à qualculer
                                                      auto dN1N2 = sqrt(squaredDistance(n1.point, n2.point));
                                                      auto dN1N3 = sqrt(squaredDistance(n1.point, n3.point));
                                                      auto dN3N3 = sqrt(squaredDistance(n2.point, n3.point));
                                                      return dN1N2 + dN1N3 + dN2N3;
                                                  }

                                                  L'énorme avantage de travailler de la sorte, étant que cela te permet, le cas échéant, de faire varier la manière dont chaque maille sera évaluée, et donc, l'interpolation que tu pourras en faire, en fonction des besoins.

                                                  Cerise sur le gâteau, cela te permettrait de faire calculer la métrique afin de t'assurer que la maille est "dans les clous" ou non ;)

                                                  Par contre, cela impose sans doute de définir plus précisément la notion de maillage (de liste de mailles), car il y a de fortes chances pour que tu veuilles d'utiliser la même fonction pour évaluer l'ensemble des mailles qu'il contient ;)

                                                  • 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
                                                    15 septembre 2020 à 12:06:43

                                                    Dire qu'on a une _liste_ de points ou de mailles est prématuré.  Pour choisir le choix du conteneur le plus adapté il faut savoir quelles seront les opérations à effectuer dessus le plus fréquemment.

                                                    Ce n'est pas indépendant de l'algorithme.

                                                    -
                                                    Edité par michelbillaud 15 septembre 2020 à 12:07:41

                                                    • Partager sur Facebook
                                                    • Partager sur Twitter
                                                      15 septembre 2020 à 13:03:05

                                                      De fait, le terme "liste" est prématuré, et j'aurais sans doute du parler "d'ensemble de" ou de "collection de" pour être "plus générique" et courir moins de risques de me tromper :D

                                                      Cependant, comme il faudra bien garder les points, les noeuds et les mailles que l'on s'apprête à  créer "quelque part en mémoire" pour être en mesure de les utiliser, il semble "cohérent" de se dire que nous devons prévoir les notions permettant de représenter les différentes "collections" dans notre code, quelle que soit la manière dont les éléments seront représentés en mémoire à l'intérieur de ces collections ;)

                                                      • 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
                                                        16 septembre 2020 à 0:07:19

                                                        J'avais promis dans mon précédent message de ne plus revenir vous importuner tant que je n'aurai pas du code à montrer, avec son "bien", son "mauvais" et ses horreurs absolues, que vous ne manquerez pas de m'indiquer - pour mon éducation en c++.

                                                        Alors 1ère question : sous quelle forme vous transmettre ledit code ? Par pièces jointes ou par copie/paste dans la "fenêtre noire" prévue dans l'éditeur de ces messages ? 

                                                        Dans ce message, je tiens juste à répondre aux questions de koala1 sur les aspects algorithmiques.

                                                        koala1 a écrit :

                                                        tbc11 a écrit:

                                                           1) Construire un maillage triangulaire du plan à partir de ces noeuds : il y a une multiplicité de maillages triangulaires différents possibles, mais il y en n'a qu'un seul qui se prête à l'interpolation - celui de Delaunay.

                                                        En es tu tout à fait sur?

                                                         Je m'appuie sur les travaux de Boris Delaunay, mathématicien russe (contrairement à ce que son nom suggère) et qui a démontré ceci en 1924. Je résume grosso-modo la problématique :

                                                        • Pour interpoler une fonction 2D [z = f(x,y)] là où on ne la connaît que sur 3 noeuds, on ne peut que formuler un modèle bilinéaire (de type z_modele = a + bx + cy) valable uniquement à l'intérieur du triangle formé par ces 3 noeuds.
                                                        • L'approximation des pentes (df/dx = b et df/dy = c) est donnée par les noeuds et la valeur de la fonction en ces noeuds. L'approximation réside dans le fait que l'on confond "pente" (dérivée partielle) et corde (accroissement fini).
                                                        • D'où l'intérêt d'avoir des triangles les plus "équilibrés" possibles : le triangle équilatéral est le meilleur pour interpoler, le triangle "aplati" (dégénérant vers 3 points alignés) est à proscrire : la formule d'interpolation dégénère en type 0/0 => donne n'importe quoi par une évaluation numérique par ordinateur.
                                                        • Quand on a un espace à mailler (cas des calculs 3D par exemple) : on maille l'espace avec des triangles corrects (suivant les bonnes pratiques des mailleurs) et cela dicte la localisation des noeuds. Puis on discrétise le problème physique sur la base de ces noeuds.
                                                        • Dans le cas où l'on subit la discrétisation (cas de relevés expérimentaux par exemple), c'est le problème inverse - le mien en l'occurrence : on subit les noeuds, et il faut "faire avec", i.e. trouver le "moins mauvais" maillage qui permettra d'interpoler.
                                                        • Delaunay a démontré que le sien est le "moins mauvais possible", i.e. celui qui, parmi tous les maillages triangulaires possibles (à noeuds imposés), maximisera le plus petit angle (entre 2 arêtes) trouvé dans l'ensemble des mailles.
                                                        • Je ne veux pas vous assommer davantage avec ces considérations, qui relèvent plutôt du forum "Mathématiques" d'Openclassroom. Si vous en connaissez les référents, ils pourront confirmer/amender ce que j'écris.

                                                        La triangulation de Delaunay est en principe unique pour un ensemble de noeuds donnés (je l'ai lu - mais je n'en ai pas la démonstration), à quelques cas dégénérés près - mais qui sont sans conséquence sur la stabilité de l'interpolation : cf. figure ci-dessous

                                                        Dans ce cas très particulier et qui a peu de chances de se produire sur un cas concret : les 2 triangulations (T1, T2) et (T'1, T'2) répondent aux conditions de Delaunay => contre-exemple sur l'unicité, mais la qualité de l'interpolation est équivalente. Donc cela ne me gêne pas.

                                                        Tout le problème est de la trouver, cette fichue triangulation de Delaunay ! Des algorithmes - dits de "géométrie algorithmique" ont été établis depuis fort longtemps (avant même que les ordinateurs n'existent - sans même parler de POO !) :

                                                        • Très faciles à mettre en oeuvre, sur la base d'une dizaine de noeuds, avec une feuille de papier, une équerre et un compas.
                                                        • Mais moins évidents à formaliser pour un (stupide) ordinateur : c'est précisément cet aspect qui m'intéresse.
                                                        • Il doit certainement exister des librairies spécialisées qui mettent en oeuvre cette méthode et probablement bien mieux que je ne le ferai : mais c'est le plaisir de conceptualiser "un truc compliqué" et une bonne occasion de me "dégrossir" en c++ que je cherche.

                                                        koala1 a écrit :

                                                        D'accord, après tout, je ne suis pas là pour te contrarier... Cependant, je voudrais être sur que l'on est sur la même longueur d'onde, et, pour cela, j'ai bien envie de te poser trois questions "qui fachent":

                                                        • Pourquoi ces trois point là précisément? ou, si tu préfère, pourquoi faire intervenir le hasard dans ce choix?
                                                        • Pourquoi dans cet ordre particulier? ou, si tu préfères, ne pourrait on pas envisager de les mettre dans leur ordre logique (8,17,23 dans le cas présent)?
                                                        • Et surtout: quelle garantie y a-t-il qu'il n'y ait aucun point dans le triangle ainsi formé?  Est-ce que cela a seulement une quelconque importance?

                                                        La version incrémentale de la construction de la triangulation de Delaunay (cf. les liens sur les articles que j'ai mis dans mes messages précédents) dit que :

                                                        • Démarre n'importe comment : 3 noeuds pris "au pif" dans la liste
                                                        • Puis suis l'algorithme... et ça finit par aboutir au résultat.
                                                        • Ces figures, avec des numéros de noeuds quelconques, étaient juste là pour illustrer le concept.
                                                        • Dans mon futur programme, je commencerai évidemment par les noeuds (1, 2, 3)... pardon (0, 1, 2). Excusez ma vieille réminiscence de Fortran 77 :D.
                                                        • Ne pas oublier que les noeuds seront préalablement lus dans un fichier de résultats expérimentaux, où aucune relation d'ordre n'existe. Donc (1, 2, 3) ne signifie rien d'un point de vue physique.

                                                        koala1 a écrit :
                                                        Euuhhh, ... Quelle est cette métrique parfaitement définie dont tu parle?


                                                        Nous sommes-là dans le coeur de la "Delaunayserie" - métrique parfaitement définie, et un peu calculatoire à mettre en oeuvre.


                                                        Le triangle T2 (8, 23, 45) est à rejeter car le cercle qui le circonscrit contient le noeud 17, qui est celui d'un autre triangle.

                                                        Ayant permuté l'arête commune (8, 23) avec le triangle T1 qui est en conflit, on obtient :

                                                        Et là, ça marche :

                                                        • Le cercle qui circonscrit T'1 (8, 17, 45) ne contient aucun noeud d'un autre triangle
                                                        • Le cercle qui circonscrit T'2 (17, 23, 45) ne contient aucun noeud d'un autre triangle

                                                        Voilà... Il ne me reste plus qu'à programmer ce critère.

                                                        koala1 a écrit :

                                                        Cependant, je dois t'avouer que ton T2 ne me semble pas vraiment beaucoup plus grand que le T2 que tu montre dans le cas2 et que tu te sens obligé de détruire pour former les mailles autrement


                                                        Oui, mais là, je n'y peux rien (à ce stade de la construction incrémentale) :

                                                        • Si le noeud tombe à l'intérieur, il n'y a pas le choix que de le relier aux 3 autres sommets du triangle qui le contient
                                                        • Il n'en reste pas moins que la nouvelle triangulaltion (T'1, T'2, T'3) est de Delaunay : tu peux essayer avec les cercles.
                                                        • Et aux stades ultérieurs, quand on introduira d'autres noeuds dans le voisinage, les triangles (T'2, T'3) sont enclins à tomber en conflit avec ces autres noeuds. Auquel cas, ils disparaîtraient dans le processus de construction.

                                                        Et si malgré tout, une fois tous les noeuds introduits, il reste des triangles "aplatis" :

                                                        • Delaunay a démontré que ce serait pire avec toute autre triangulation
                                                        • Du coup, ce sera à l'utilisateur de contrôler son maillage, de supprimer le(s) noeud(s) qui aboutissent à cette situation, et de recommencer le maillage.

                                                        Du coup, tu vas me dire que cela contredit "l'invariance des noeuds" que je pose comme principe...

                                                        • Oui, mais ce sera un autre processus de triangulation, sur la base d'une liste de noeuds plus réduite
                                                        • De toute manière, je n'en suis pas encore rendu-là, à savoir la critique d'un maillage obtenu à noeuds donnés : "Step by Step" ne cessez vous tous de me répéter !

                                                        -
                                                        Edité par tbc11 16 septembre 2020 à 7:09:11

                                                        • Partager sur Facebook
                                                        • Partager sur Twitter
                                                          16 septembre 2020 à 6:54:22

                                                          Je reviens sur l'histoire de friend "Qui serait signe d'une mauvaise conception".

                                                          Dans des petits exercices, c'est souvent vrai. Mais faut pas généraliser.

                                                          Ça repose sur l'idée qu'un programme  ca devrait êre fait avec des classes qui ont

                                                          • Une interface de programmation accessible au monde entier
                                                          • Des détails internes visibles seulement depuis le code de la classe.

                                                          C'est pas tres réaliste, parce que ça suppose une indépendance d'implémentation totale entre les classes.

                                                          En Java c'est plus raisonnable. Si on développe un truc comme le tien, on en fera peut être un package avec des classes pour représenter les sommets, les mailles etc. Les sommets, les mailles sont implémentés de facon interdependante, et le code des uns peut tirer profit des détails internes des autres. 

                                                          Par contre la visibilité des détails est limitée au package. L'utilisateur des classes n'y a pas accès, c'est pas ses affaires.

                                                          • Partager sur Facebook
                                                          • Partager sur Twitter
                                                            16 septembre 2020 à 7:53:56

                                                            Pas tout lu, c'est trop long.

                                                            michelbillaud a écrit:

                                                            Je reviens sur l'histoire de friend "Qui serait signe d'une mauvaise conception".

                                                            Dans des petits exercices, c'est souvent vrai. Mais faut pas généraliser.

                                                            Ça repose sur l'idée qu'un programme  ca devrait êre fait avec des classes qui ont

                                                            • Une interface de programmation accessible au monde entier
                                                            • Des détails internes visibles seulement depuis le code de la classe.

                                                            C'est pas tres réaliste, parce que ça suppose une indépendance d'implémentation totale entre les classes.C'est pas tres réaliste, parce que ça suppose une indépendance d'implémentation totale entre les classes.

                                                            En Java c'est plus raisonnable. Si on développe un truc comme le tien, on en fera peut être un package avec des classes pour représenter les sommets, les mailles etc. Les sommets, les mailles sont implémentés de facon interdependante, et le code des uns peut tirer profit des détails internes des autres. 

                                                            Par contre la visibilité des détails est limitée au package. L'utilisateur des classes n'y a pas accès, c'est pas ses affaires.

                                                            Pour le premiere partie, je suis d'accord avec toi. On ne peut pas etre aussi absolue et dire que friend est toujours le signe d'une mauvaise pratique.

                                                            Pour la seconde partie, on peut le faire en placant une partie du code dans des classes/repertoire explicitement note private ou internal. Voire ne pas distribuer les headers quand la lib est compilée. (Par exemple dans Qt. Ou FILE du C). Et si elle n'est pas compilée, on donne aussi les private/internal et c'est le probleme de l'utilisateur s'il fait de la merde. (Par exemple dans la lib standard, qui contient pleins headers qui ne sont destines a etre appeles directement).

                                                            Donc globalement, en C++, on peut concevoir un code en termes de packages, sous packages, parties privates et publiques, etc. C'est qu'une question d'organisation du code. Mais il est clair qu'on ne peut pas avoir que des classes completement independante en termes d'implementation.

                                                            -----------------------------------------------------------------------------------

                                                            EDIT : quand meme, parce que ca m'amuse beaucoup. Je cherchais le mot pour decrire cette conversation, je l'ai sur le bout de la langue...

                                                            koala01 a écrit:

                                                            Il est vrai que c'est presqu'un art que d'exprimer simplement ses besoins, ses attentes, ses convictions.

                                                            Ah, voila ! Simplicité. C'est ce qui decrit le mieux tes messages :D

                                                            -----------------------------------------------------------------------------------

                                                            EDIT2 :

                                                            tbc11 a écrit:

                                                            La triangulation de Delaunay est en principe unique pour un ensemble de noeuds donnés (je l'ai lu - mais je n'en ai pas la démonstration), à quelques cas dégénérés près - mais qui sont sans conséquence sur la stabilité de l'interpolation 

                                                            Quand je vois l'image :

                                                            je me dis que selon l'ordre (aleatoire) de tirage des noeuds, on va obtenir des resultats differents.

                                                            • 8-17-23-45 => T1, T2 et T3
                                                            • 8-17-45-23 => T1 et T3
                                                            • 8-23-45-17 => T1 et T2
                                                            • etc

                                                            Pas sur que ce soit tres unique. Pas sur non plus de savoir pourquoi tu parles d'interpolation. En tout cas, il existe d'autres algos de triangulation. Cf wikipedia ou la doc de CGAL.

                                                            Mais bon, comme c'est pour un exo, osef.

                                                            -
                                                            Edité par gbdivers 16 septembre 2020 à 8:27:12

                                                            • Partager sur Facebook
                                                            • Partager sur Twitter
                                                              16 septembre 2020 à 10:25:04

                                                              Bonjour,

                                                              michelbillaud a écrit : Je reviens sur l'histoire de friend "Qui serait signe d'une mauvaise conception".

                                                              En ce qui concerne mon problème, les réponses précédentes font que je n'aurai finalement pas besoin de cette amitié. Je compte procéder avec :

                                                              • des noeuds encapsulés, afin d'éviter toute modification intempestive de leurs coordonnées, qui rendraient le maillage (géré ailleurs) incohérent
                                                              • des accesseurs inline qui ne coûteraient pas plus de temps qu'un accès direct aux coordonnées (cf. message de lmghs)

                                                              gbdivers a écrit: Quand je vois l'image : je me dis que selon l'ordre (aleatoire) de tirage des noeuds, on va obtenir des resultats differents.

                                                              • 8-17-23-45 => T1, T2 et T3
                                                              • 8-17-45-23 => T1 et T3
                                                              • 8-23-45-17 => T1 et T2
                                                              • etc 

                                                                      Pas sur que ce soit tres unique. 

                                                              Suivant l'ordre de tirage des noeuds, le résultats intermédiaires (i.e. les chemins suivis lors de la construction incrémentale) seront différents, comme tu le soulignes. Mais quand on aura introduit tous les noeuds, la solution finale sera unique - dixit les matheux. De toute façon, je verrai bien s'ils disent juste.

                                                              Question (de béotien) : comment faites-vous pour présenter correctement les citations des messages précédents, avec "machin à écrit:" et la jolie barre grise isolant bien le texte extrait ? Je n'y arrive pas avec les boutons du menu. 




                                                              • Partager sur Facebook
                                                              • Partager sur Twitter

                                                              Définir correctement les classes

                                                              × 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