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 ?
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"
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.
- 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
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
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
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 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.
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
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 ?)
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).
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
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 )
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
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 .
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).
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
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
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 )
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
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 .
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.
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?
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
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.
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 :
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.
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.
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 )
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
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
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 :
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à ?
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 <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_.x+diffX >= 0 && "abscisse invalide detecte!");
/* l'ordonnée ne peut pas être inférieur à 0 */
assert(coordinates_.y+diffY >= 0 && "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
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
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 ???
- Edité par koala01 14 septembre 2020 à 6:06:45
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
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 T1et 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.
> 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à....
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 T1et 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
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
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
parce que cela fait moins de code à écrire et que je suis de nature fainéante
Parce que cela permet de rendre le code "plus compact" et donc plus facile à lire
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 )?
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
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 )
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 T1et 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 .
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 !
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.
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.
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.
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.
× 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.
Eug
Discord NaN. Mon site.
Discord NaN. Mon site.