IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

 C++ Discussion :

membre constexpr et héritage


Sujet :

C++

  1. #1
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut membre constexpr et héritage
    Bonjour à tous,

    Dans le cadre de mon projet, je souhaite pouvoir identifier certaines classes avec des ID uniques déterminés à la compilation, statiques à classe considérée et surtout non modifiables.

    Jusque là, je m'en sort bien. J'ai une fonction de hachage qui me permet, via une macro, de réaliser ce que je souhaite :
    #define SIGNATURE_TAG(val) static constexpr std::size_t signature = strhash(#val)Par contre, là où ça se corse, c'est que ces différentes classes doivent hériter d'un parent permettant d'accéder à ces valeurs. Si je reprends la macro ci-dessus, j'ai :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #define SIGNATURE_TAG(val) static constexpr std::size_t signature = strhash(#val)
     
    struct A
    {
    	/* que faire ici pour pouvoir accéder à "signature" */
    	//[...]
    };
     
    struct B: A
    {
    	SIGNATURE_TAG("premier hash");
    };
     
    struct C: A
    {
    	SIGNATURE_TAG("second hash");
    };
     
     
    int main()
    {
    	std::vector<A> vals;
    	vals.push_back(B);
    	vals.push_back(B);
    	vals.push_back(C);
     
    	for(auto a:vals_)
    	{
    		std::cout << a::signature << std::endl;
    	}
     
    	return 0;
    }
    si je renseigne une variable "signature" dans A, alors j'ai un conflit, mais si je ne le renseigne pas, je ne peux donc pas accéder aux valeurs nécessaires pour le bon déroulement de mon programme...

    Autre bug que je retrouve, en parallèle, c'est lors du passage d'une des structures (B, ou C) via référence ou pointeurs à une fonction, je ne récupère pas les mêmes valeurs. Ceci me surprends beaucoup, aussi je pense que cette dernière problématique est en partie liée à la première.


    Une idée ?

    Merci d'avance.

  2. #2
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 115
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 115
    Points : 32 965
    Points
    32 965
    Billets dans le blog
    4
    Par défaut
    Salut,

    en gros tu veux réimplémenter du RTTI ?
    Ce que je fais dans ces cas c'est une macro DECLARE comme tu l'as, mais il faut aussi une fonction virtuelle qui retourne ceci.
    Grosso merdo rapidement non testé du genre :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    #define DECLARE_RTTI(classname, parent) \
    static constexpr size_t ClassId = hash(#classname ## #parent); \
    size_t GetClassId() const override { return ClassId; }
     
    #define DECLARE_RTTI_ROOT(classname) \
    static constexpr size_t ClassId = hash(#classname ## #parent); \
    virtual size_t GetClassId() const { return ClassId; }
    Ensuite tu as généralement les fonction IsA, Cast, etc qui s'ajoutent.

    Sinon ce pourrait aussi être le moment d'utiliser du CRTP.

    Mais on n'a pas assez d'infos pour choisir à ta place..
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  3. #3
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    vals ne contient pas des B ou C, mais des A. L'héritage devrait se faire sous 2 conditions:
    - une base qui possède des fonctions membres virtuelles et par conséquent pas d'opérateur et constructeur de copie, mais un destructeur virtuel. Sans ces interdits, il y aura slicing (perte d'info par changement de type via copie).
    - un héritage privé dans le cas contraire

    Seul le point 1 permet de garder des informations et comportement à travers une classe de base. Par conséquent, vals ne peut pas être un std::vector de A, mais de référence sur A (std::reference_wrapper, unique_ptr, raw pointer, etc)..

    Ensuite a dans la boucle est de type A et signature un membre statique. Par conséquent, le compilateur va chercher le membre dans A. Si a était une référence qui pointe sur un type B, le compilateur irait quand même chercher signature dans A, car a serait une référence de type A& et donc de type A: les membres statiques ne sont pas associés à une instance mais au type utilisé.

    Comme on ne sait absolument pas le besoin, je balance juste en vrac quelques méthodes utilisées:
    - std::variant
    - une espèce de std::any
    - du polymorphisme sur une fonction signature
    - du polymorphisme et un passage du hash en paramètre au constructeur de A.

  4. #4
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Ok, merci pour tous ces retours. A la base, tenant tout particulièrement à conserver ces structures extrêmement triviales, je souhaite pouvoir avoir un accès direct aux membres, sans passer par des fonctions. Ce que je cherche à faire avec ça est un ECS (restant simpliste mais fonctionnel, sans prétention).

    Le but final de tout ça, c'est de pouvoir créer des composants simples, avec un type commun permettant de les stocker dans un unique vecteur (c'est plus pratique quand on commence à avoir pas mal de composants...), mais conservant une information unique permettant d'identifier chaque type de composant (plus loin dans le code, ces derniers seront finalement classés par type, ou plus exactement, des pointeurs ou des références vers les composants seront classés par types).

    Tel que je m'imagine la chose, lors du traitement, je pensais faire une fonction de ce type :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<typename CTYPE, typename ...REST>
    bool processNewComponent(Component & cmp)
    {
    	if(CTYPE::signature == cmp.signature)
    	{
    		return true;
    	}
    	else
    	{
    		return processNewComponent<REST...>(cmp);
    	}
    }

    de cette manière on sait si le composant traité fait parti ou non du vecteur de recherche.

    Étant encore en train de travailler sur ce code, je n'ai pas vraiment de version complète ou finale à fournir. Pour le moment, je cherche à définir une première orientation et valider (ou non) une idée.


    Au passage, effectivement, je me rends compte que, dans un vecteur de A (pour reprendre le premier exemple), la valeur retournée est bien celle de A::signature... J'obtiens donc bien effectivement, soit A::signature, soit un joli message de mon éditeur de lien qui me dit "undefined reference to B::signature"...

  5. #5
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Salut,

    J'ai une question idiote : pourquoi voudrais tu faire quelque chose dans le genre

    Car, a priori, tu n'en as absolument pas besoin! Je m'explique:

    Chaque objet connaît obligatoirement le type réel dont il est issu, et donc, les fonctions polymorphes -- les fonctions virtuelles déclarées dans la classe de base, dont le comportement est redéfini au niveau de la classe dérivée -- suffisent amplement dans la plupart des cas.

    Au pire, tu te retrouves dans une situation dans laquelle tu as "été assez bête" que pour perdre l'information directe concernant le type réel de l'objet. Par exemple, parce que tu as décidé de placer tous les objets créés dans une collection de "pointeurs vers le type de base". Et, dans ce cas, la seule solution raisonnable de travailler (qui respecte l'OCP) sera de profiter du fait que chaque objet connaît son type réel pour passer par le double dispatch.

    Toute tentative de sélection d'un comportement particulier sur base d'une information de type est -- par nature -- vouée à termes à la catastrophe. Pourquoi, me demanderas tu sans doute?

    Hé bien, tout simplement parce que tu vas sans doute commencer par deux (trois, quatre, cinq) classes qui renverront les identifiants [c]class1[c], [c]class2[c]... [c]class5[c], et par un comportement qui ressemblerait à un test à choix multiple (ou une série de if ..; else) proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    switch(obj->id()){
        case class1:
            static_cast<Class1 *>(obj)->truc();
            break;
        case class2:
            static_cast<Class2 *>(obj)->machin();
            break;
           /* ... */
        case class5:
            static_cast<Class5 *>(obj)->bidule();
            break;
    }
    Jusque là, tu n'aurais sans doute pas trop de problème. Sauf que ce genre de code va se retrouver, au fil du temps, à cinq, dix, trente endroits différents dans ton code.

    Puis, un jour, tu vas te rendre compte que tu as besoin d'une nouvelle classe (dont l'identifiant est class6) qui exposent ses propres comportements tout à fait spécifiques.

    Avec "un peu de chance", tu te souviendra du dernier endroit où tu as mis ce genre de logique en place. Et tu iras donc directement à cet endroit du code pour rajouter le
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    case class6:
        static_cast<Class6 *>(obj)->brol()
        break;
    correspondant.

    C'est cool, non Mais, tu ne pensera absolument pas aux ... (vingt-)neuf autres endroits du code qui nécessitent une adaptation similaire. Et il n'y aura absolument rien qui te permette de te rendre compte de ton oubli.

    La seule chose, c'est que, à un moment de l'exécution, tu pourras éventuellement te rendre compte que ton objet de type Classe6 aurait du subir une modification quelconque, aurait du faire partie d'une action quelconque, et que ce n'est pas le cas. Et le pire de l'histoire, c'est que, avant que tu ne t'en rende compte, il pourrait très bien s'être déjà écoulé plusieurs mois!

    Tu vas donc perdre deux bonnes heures dans une folle séance de débug (qui te fera sans doute attraper un ou deux cheveux gris par la même occasion) avant de trouver l'endroit du code où tu dois ajouter le choix class6. Et, après, tu sera content et fer de toi, car, selon toute évidence, ton programme devrait fonctionner correctement! Mais non! Parce qu'il y a toujours ... (vingt-)sept autres endroits dans le code dans lesquels ce choix n'apparaît pas. Et il te faudra sans doute encore plusieurs mois avant de te rendre compte que ce choix manque "quelque part".

    Au final, une décision prise aujourd'hui, qui peut sembler parfaitement cohérente, va revenir te hanter et te pourrir la vie pendant des années (en fait, aussi longtemps que l'application sera utilisée), en (re)venant te péter à la figure de manière très régulière. Ce n'est pas le genre de chose que l'on souhaite. Et c'est d'autant plus idiot qu'il existe suffisamment de moyens d'éviter le problème.

    Maintenant, tu as peut-être d'excellentes raisons pour vouloir identifier spécifiquement les différentes classes. Par exemple, pour mettre en place une sorte de factory, mais à la condition expresse que cette identifiant ne serve qu'à cela! Et c'est la raison pour laquelle je te pose la question de savoir pourquoi tu voudrais faire une chose pareille. Qui sait, avec un peu de chance, nous pourrons t'indiquer le moyen de t'en passer une fois que nous aurons compris le besoin que tu cherches à remplir au travers de cette solution
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  6. #6
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Je rejoins Koala : pourquoi veux-tu faire ça ?

    Identifier une classe, ça peut se faire facilement avec https://en.cppreference.com/w/cpp/language/typeid

    Vouloir dans des classes dérivées connaitre les identifiants des classes mères, ça ressemble à une mauvaise solution à un mauvais problème. Avant de te lancer dans un truc compliqué, reviens à la base pour voir si tu as vraiment besoin de faire ça. Dis-nous en plus, qu'on puisse te donner un avis plus global

  7. #7
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Bonjour à tous, et merci pour ces échanges.

    En fait, je réuni ici plusieurs objectifs, et, ci-dessous, je vous présenterais en fin la finalité (le code) de ce que je souhaite concrètement mettre en place (une fois que j'aurais répondu à la problématique posée dans ce fil).

    • Les composants, de type différents et substituables à un "composant originel" doivent pouvoir être stockés sous un seul et même container
    • Les systèmes peuvent être créés de manière simple, à volonté, sans avoir à se soucier de la manière dont ils vont chercher les composants nécessaires à leur fonctionnement
    • Les entités sont, pour moi, une classe qui contiens à minima un index d'entité + une liste de pointeurs vers les composants qui "structurent" la-dite entité.


    pour mettre tout ça en place, je présenterais donc, un "Manager", qui n'est finalement rien d'autre qu'une interface au sens propre (ou figuré ???) du terme, qui servira de point d'entré unique ou presque à l'utilisateur. Cette classe ne me semble, à priori, triviale pour le moment ; j'en présenterais tout de même le code actuel, permettant de voir comment sont stockés les différents éléments.

    Classe Manager :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Manager
    {
    private:
     
     
    	std::vector<std::vector<BaseComponent> > m_component;
    	std::vector<ISystem> m_system;
     
    public:
     
    	// no parameters for CTors / DTors ?
    	Manager() = default;
    	~Manager() = default;
     
     
    	Entity & createEntity();
     
    	bool addNewComponent(BaseComponent & newComponent);
     
    	//NEXT : finish implementation... 
     
    };
    Bien entendu, pour tout ça nous devons présenter aussi les composants, tels que je les ait déjà présentés plus haut :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class BaseComponent
    {
    	// ?????
    };
     
    class myNewComponent: BaseComponent
    {
    	static constexpr std::size_t signature = hash(myNewComponent);
    	// or
    	// signature = get_typed_id<myNewComponent>();
    };

    Rien de bien nouveau ici. Cependant, voici la classe BaseSystem ci-dessous :


    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    template<typename ...SYSTEM_CTYPES>
    class BaseSystem: public ISystem
    {
    protected:
     
    	using componentTuple_t = std::tuple<SYSTEM_CTYPES&...>;
    	std::vector<componentTuple_t> m_componentTuples;
     
     
    public:
    	explicit BaseSystem() {}
    	virtual ~BaseSystem() {}
     
    	virtual bool addNewComponent(componentTag_t const & tag, BaseComponent* newComponent/*, Other thing ?*/) override final;
     
    	template<std::size_t PROCESS_ID, typename CTYPE, typename ...OTHER_CTYPES>
    	bool processNewComponent(componentTag_t const & tag, BaseComponent* newComponent/*, Other thing ?*/) ;
     
    	template<std::size_t PROCESS_ID>
    	bool processNewComponent(componentTag_t const & tag, BaseComponent* newComponent/*, Other thing ?*/);
     
    	// virtual void update() = 0; // implement it in the final system only
     
     
    };
     
     
     
    template<typename ...SYSTEM_CTYPES>
    bool BaseSystem<SYSTEM_CTYPES...>::addNewComponent(componentTag_t const & tag, BaseComponent* newComponent)
    {
    	if(processNewComponent<0, SYSTEM_CTYPES...>(tag, newComponent/*, Other thing ?*/))
    	{
    		return true;
    	}
     
    	return false;
    }
     
    template<typename ...SYSTEM_CTYPES>
    template<std::size_t PROCESS_ID, typename CTYPE, typename ...OTHER_CTYPES>
    bool BaseSystem<SYSTEM_CTYPES...>::processNewComponent(componentTag_t const & tag, BaseComponent* newComponent/*, Other thing ?*/)
    {
            DBG_ASSERT(CTYPE::signature != BaseComponent::signature)
    	DBG_ASSERT(tag != BaseComponent::signature)
     
    	if(CTYPE::signature == tag)
    	{
    		// do something using PROCESS_ID ( maybe store the new component in a tuple or a vector thanks to PROCESS_ID ?)
    		return true;
    	}
    	else
    	{
    		return processNewComponent<PROCESS_ID+1, OTHER_CTYPES...>(tag, newComponent);
    	}
    }
     
    template<typename ...SYSTEM_CTYPES>
    template<std::size_t PROCESS_ID>
    bool BaseSystem<SYSTEM_CTYPES...>::processNewComponent(componentTag_t const & tag, BaseComponent* newComponent/*, Other thing ?*/)
    {
    	// same as before but used for termination. In this case, no match occurred
    	// in this case, just return false...
    	return false;
    }

    On constate donc, dans ce fonctionnement, que le nouveau composant est passé en paramètre de la méthode addNewComponent(). Il est donc important de pouvoir récupérer ici n'importe quel type de composant. Cependant, idiot que je suis, je me demande si je ne peux pas passer ce type en template + déclaration implicite.... Cela devrait fonctionner. A tester.
    edit : dans le code ci-dessus, j'ai pris la liberté de passer en paramètre aussi le tag. Cette implémentation implique, en fait, que la signature du composant soit récupérée avant d'entrer dans la fonction. Néanmoins, le problème reste le même dans le sens où je vais devoir extraire cette signature à un moment ou un autre. Cependant, je peux imaginer que ce tag est fourni / stocké quelque-part, en dehors du composant lui-même, lors de l'instanciation de l'objet... A voir, cela fait parti d'un axe de recherche... En procédant de cette manière, je devrais pouvoir me dépatouiller d'une manière ou une autre...


    Comme demandé : "pourquoi ne pas donner directement le/les bon composants au moment de l'ajout au système ?" J'y ai pensé aussi... Néanmoins, que ce soit le manager ou le système, il faudra bien que l'un ou l'autre se colle à cette tâche. De plus, il est tellement plus simple de dire au manager :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    for(auto s:m_system)
    {
    	s.addNewComponent(&simpleNewComponent);
    }
    de cette manière, les systèmes intéressés récupèrent le pointeur, pas les autres.

    Enfin, voici donc comment j’imagine la création d'un système (final) :


    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class WonderfullSystem: BaseSystem<BobComponent, AliceComponent>
    {
    public:
     
    	virtual void update() override final;
    	{
    		for(auto a:m_componentTuples)
    		auto currentBob = std::get<BobComponent>(a);
    		auto currentAlice = std::get<AliceComponent>(a);
     
    		// make Bob and Alice do whatever you want with... 
    	}
    };
     
     
    Manager myManager;
    myManager.addSystem(WonderfullSystem{});

    Concernant les systèmes, bien qu'il n'y ait ici que le squelette du code final, on s’aperçoit que toute l'implémentation est triviale de A à Z (c'est peut être trop light justement ?), et la création d'un nouveau système est on ne peut plus directe et explicite.


    Tout ça pour en arriver au fait que, la classe BaseSystem fait apparaitre : CTYPE::signatureCTYPE représente une des potentielles multiples classes qui définissent justement l'essence de notre système. D'où l'importance que chaque composant ait un identifier unique par type (d'où le static constexpr std::size_t signature). En parallèle, lorsque l'on ajoute donc de nouvelles valeurs pour un composant donné, a un système, ce composant est passé en paramètre, d'où l'importance que chaque composant puisse se substituer à un composant neutre (le BaseComponent).


    En ce qui concerne les solutions proposés, effectivement, je ne me lancerais pas dans un switch() ^^ c'est en très grande partie pour éviter ça que je cherche une solution. Cependant, qu'il s'agisse du type, d'une chaîne de caractère, un hash ou un ticket de loto, peu importe la manière, je dois pouvoir être capable d'identifier clairement le type d'un objet afin de réaliser des comparaisons (!= / == / || / && etc..).

    De ce que j'ai pu comprendre du RTTI de la stl, l'utiliser quand on fait un ecs, c'est un peu comme construire une formule1 en utilisant du plomb au lieu du carbone... Je n'utiliserais donc pas de std::typeid ici. Cependant, je viendrais corriger ici un point qui me semble important : je ne souhaite pas que les enfants connaissent le type de leurs parents, ils n'en ont strictement rien à faire. C'est justement tout l'inverse que je souhaite faire : pouvoir récupérer, depuis le parent, le type de l'enfant auquel il est rattaché.

    Pour ça, effectivement, le CRTP peut sembler intéressant aux premiers abords ; cependant, cela implique que la classe parente est donc... un template. Ce que je ne veux surtout pas dans le sens où la classe BaseComponent a vocation à être substituée par ses enfants.


    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    struct Base
    {
    public:
    	static constexpr std::size_t tag{0};
    };
     
    struct A:Base
    {
    	static constexpr std::size_t tag{1};
    };
     
    struct B:Base
    {
    	static constexpr std::size_t tag{2};
    };
     
     
    int main()
    {
     
    	std::vector<Base> m_vec;
     
    	m_vec.push_back(A{});
    	m_vec.push_back(B{});
    	m_vec.push_back(B{});
    	m_vec.push_back(Base{});
     
    	for(auto v:m_vec)
    	{
    	    std::cout << v.tag << std::endl;
    	}
     
     
    	return 0;
    }
    et le retour : "0 0 0 0" --> Que les id de la structure "Base" !!! A vrai dire, je m'en fiche que la structue "Base" ait un identifiant (en vrai) mais si je le lui enlève, le bout de code ci-dessus ne compile plus...
    Le retour souhaité ici est " 1 2 2 0" ...

    Je vais continuer à me triturer les méninges autour de ça... Je vais bien finir par trouver une solution.

    Merci à vous !

  8. #8
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Je crois que je t'ai déjà fait cette réflexion. Je me répète donc sans crainte : dans un système ECS, tu oublie l'approche orientée objet!!!

    La liaison qu'il y a entre une entité (qui n'est qu'un identifiant numérique) et un composant s'obtient grâce à la possibilité de représenter une paire de donnée entité + composant

    Pour chaque liaison entité + composant que tu trouve, tu crées l'équivalent d'une table dans une base de donnée, sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    entité | composant (point)
           | x  | y 
    ---------------------------
    1      | 10 | 20
    15     | 45 | 60 
    65     | 98 | 13
     
    // UNE AUTRE TABLE
    entité | composant (vitesse)
    ----------------------
    1      | 0.00
    15     | 8.9
    65     | 3.1415
     
    // ENCORE UNE AUTRE TABLE
    entité | composant (accélération)
    --------------------------------
    1      | 8.886
    15     | 4.32
    65     | 0.0000
    A partir de là, tu peux éventuellement créer une "vue", un "instantané" des valeurs qui t'intéressent dans un cas bien particulier.

    L'idéal étant que chaque référence à un composant soit représentée par l'indice de l'élément qui nous intéresse dans sa propre table, par exemple
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    entité | position | vitesse | accélération
    -----------------------------------
    1      | 1        | 1       | 1
    15     | 2        | 2       | 2
    65     | 3        | 3       | 3
    De cette manière, si tu as un service qui a besoin de ces trois informations pour chacune des entités visées, il peut aller chercher les informations qui l'intéresse assez facilement.

    Le seul élément qui puisse éventuellement (et encore, il faudra me convaincre de l'utilité de la chose), ce serait la partie "systems", qui pourrait éventuellement prendre une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SystemBase{
    public:
        virtal void update(double enlapsedTime) const = 0;
    };
    class SubSystemOne : public SystemBase{
    public:
        void update(double enlapsedTime) const overrride;
    };
    class SubSystemTwo : public SystemBase{
    public:
        void update(double enlapsedTime) const overrride;
    };
    Mais tu n'as absolument aucune raison d'envisager le recours à l'héritage (ou pire, au RTTI) dans une approche de type ECS
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  9. #9
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    En ce qui concerne les composants, je suis entièrement d'accord ; d'ailleurs, j'ai finalement trouvé une solution me permettant de me passer complètement de "BaseComponent". Ces derniers sont donc redevenus de simples PODs ne contenant que des Data. Par contre, mes anciennes implémentations de tables permettant de gérer multiples types de de composants doivent clairement être revues pour gagner en robustesse

    Je sens que je vais finir par me faire taper dessus mais, pour ce qui est des systèmes, je ne vois pas trop le mal qu'il y a à hériter.... Oui, l'interface pour les systèmes est facultative, ok, j'enlève un niveau ; c'est un point que je partage quant à l’inutilité de la chose et il est n'est pas absolument nécessaire de les regrouper dans un container; par contre, pour ce qui est du reste mon orientation semble correcte. Certes, on peux toujours mieux faire ; mais dans le cas précis, à moins qu'il y ait des impacts particuliers sur les performances, impacts que je ne connais pas encore, je ne vois pas pourquoi j'irais m'em***er à oublier la POO là où elle rends le code plus lisible / accessible / maintenable, sans pour autant venir impacter la performance du programme.

    dans le code que tu présentes :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SystemBase{
    public:
        virtal void update(double enlapsedTime) const = 0;
    };
    class SubSystemOne : public SystemBase{
    public:
        void update(double enlapsedTime) const overrride;
    };
    class SubSystemTwo : public SystemBase{
    public:
        void update(double enlapsedTime) const overrride;
    };
    Qu'est-ce qui te chagrine à ce point là-dedans ?


    La liaison qu'il y a entre une entité (qui n'est qu'un identifiant numérique) et un composant s'obtient grâce à la possibilité de représenter une paire de donnée entité + composant
    Entièrement ok avec toi sur ce point aussi. Je dois encore réfléchir à comment gérer mes entités avec ce nouveau schéma. On peut imaginer toute sorte de chose mais dans tous les cas, la description que tu fais ici restera valable dans tous les cas...

  10. #10
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Je sens que je vais finir par me faire taper dessus mais, pour ce qui est des systèmes, je ne vois pas trop le mal qu'il y a à hériter....
    Il n'y a rien de "mal", vu qu'une solution basée sur l'héritage serait malgré tout correcte.

    Mais il y a une différence entre une solution "correcte" et une solution "efficace"...

    Or, il se fait qu'une solution à base d'héritage est -- très certainement -- moins efficace. Ce qui est d'autant plus dommage que ce n'est pas ... indispensable

    L'un de tes principaux soucis lorsque tu crées un jeu, c'est de faire tenir la totalité de la logique qui doit être exécutée dans un délais de l'ordre de 0.016 seconde avoir une chance de respecter les 60 fps.

    Si tu te trouves face à une situation dans laquelle deux solutions correctes ne se différencient que par un délais de quelques microsecondes d'écart, tu as très largement intérêt à choisir celle qui demande le moins de temps à l'exécution, car ce
    sont les petits ruisseaux qui font les grands fleuves
    et que ces quelques microseconde multipliés par un nombre suffisamment important peut faire "toute la différence"
    Oui, l'interface pour les systèmes est facultative, ok, j'enlève un niveau ; c'est un point que je partage quant à l’inutilité de la chose et il est n'est pas absolument nécessaire de les regrouper dans un container;
    Poses toi la question inverse : pourquoi devrais-tu payer le surcoût issu d'une fonction virtuelle si c'est pour ne pas en tirer profit en regroupant les différentes instances dans un container "de pointeurs sur la classe de base
    par contre, pour ce qui est du reste mon orientation semble correcte. Certes, on peux toujours mieux faire ; mais dans le cas précis, à moins qu'il y ait des impacts particuliers sur les performances, impacts que je ne connais pas encore,
    Voilà justement le noeud du problème : l'utilisation de fonctions virutelles, qui est la condition sine qua non pour disposer du polymorphisme (d'inclusion)...

    Si tu décidais de créer des classes -- sans leur imposer la moindre hiérarchie -- exposant toute une fonction membre "classique" ou une fonction membre statique -- ce qui pourrait être encore mieux dans un certain sens-- (portant le même nom) sans vouloir avoir recours à la notion de polymorphisme (d'inclusion), cela ne me poserait absolument aucun problème.

    Pour moi, que tu choisisse d'avoir des classes proches de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class SubSystem1{
    public:
        void update(double enlapsedTime) const;
    };
    class SubSystem2{
    public:
        void update(double enlapsedTime) const;
    };
    /* ... */
    class SubSystemN{
    public:
        void update(double enlapsedTime) const;
    };
    ou que tu choisisse d'utiliser des classes prenant la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class SubSystem1{
    public:
        static void update(double enlapsedTime) const;
    };
    class SubSystem2{
    public:
        static void update(double enlapsedTime) const;
    };
    /* ... */
    class SubSystemN{
    public:
        static void update(double enlapsedTime) const;
    };
    ou même que tu choisisse d'avoir recours à des fonctions libres proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void update1(double enlapsedTime);
    void update2(double enlapsedTime);
    /* ... */
    void updateN(double enlapsedTime);
    Pour moi, cela revient exactement au même (pense juste au fait que les fonctions membre "normales" (non statiques) implique la transmission d'un pointeur sur l'objet en tant que premier paramètre implicite, ce qui peut provoquer un léger surcoût)


    Mais, dés que tu fais entrer le polymorphisme d'inclusion en jeu, dés que tu décide d'avoir recours au mécanisme qui permet l'utilisation de fonctions virtuelles, tu te retrouves à devoir traîter avec ce que l'on appelle une "table de fonctions virtulle" (en anglais, on parle de VTable) qui permet de choisir ... l'implémentation de la fonction qui est effectivement appelée sur base du type réel de l'objet à partir duquel la fonction est appelée.

    Et ca, ben, ca va systématiquement demander quelques microsecondes si précieux
    je ne vois pas pourquoi j'irais m'em***er à oublier la POO là où elle rends le code plus a) lisible / b) accessible / c) maintenable, sans pour autant venir impacter la performance du programme.
    a) La POO n'a rien à voir avec la lisibilité du code! Au contraire, elle rend le code généralement plus verbeux en imposant de fournir les "noms pleinement qualifié" (void MaClasse::nomDeLaFonction(/* paramètres*/)) pour l'implémentation

    b) Le seul avantage que la POO apporte à ce niveau est la possibilité de créer des fonctions membres, dont les déclarations seront donc -- naturellement -- regroupée au sein de la définition de la classe, et donc dans un fichier d'en-tête unique, ce qui nous incitera "tout aussi naturellement" à en regrouper les implémentations au sein d'un fichier d'implémentation unique.

    Tu peux facilement faire pareil avec des fonctions libres, et ce sera tout aussi naturel

    Eventuellement, on peut également considérer l'obligation d'avoir recours aux noms pleinement qualifiés dans les fichiers d'implémentation comme un avantage ... Mais ce n'est pas spécifique à la POO: les espaces de noms permettent exactement la même chose pour les fonctions membres, si le besoin s'en fait sentir.
    c)Ce qui rend un code maintenable, c'est principalement:
    • le respect du SRP
    • le respect de l'OCP
    • le respect de la loi de Déméter
    • (dans une moindre mesure) le respect de l'ISP
    • (dans une moindre mesure) le respect du DIP
    • l'obtention d'un code "auto commenté" au travers d'un choix cohérent des noms (de fonctions, de types de données et des données manipulées)

    Tu remarqueras que le LSP ne fait pas partie de la liste. Or, c'est le seul principe spécifique à la POO, vu que c'est celui qui préside à la notion de substituabilité (la capacité de transmettre un objet de type B à une fonction qui s'attend à manipuler un objet de type A)

    J'espère par ces mots t'avoir convaincu que la POO n'a absolument rien à voire dans l'histoire

    dans le code que tu présentes :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SystemBase{
    public:
        virtal void update(double enlapsedTime) const = 0;
    };
    class SubSystemOne : public SystemBase{
    public:
        void update(double enlapsedTime) const overrride;
    };
    class SubSystemTwo : public SystemBase{
    public:
        void update(double enlapsedTime) const overrride;
    };
    Qu'est-ce qui te chagrine à ce point là-dedans ?
    Je crois avoir donné tous les éléments de réponse à cette question, mais pour être sur, je vais en dresser le récapitulatif:
    1. c'est inutile : tu n'as aucune raison de le faire étant donné que tu admets toi-même l'inutilité de regrouper tes systèmes dans une collection
    2. cela présente un surcoût non négligeable du à la virtualité des fonctions
    3. cela oblige à avoir une instance de chaque système, alors qu'ils représentent tous une notion "plus abstraite" (comprends : qui ne représente pas une donnée à proprement parler)
    4. les mécanismes de gestion dynamique de la mémoire que sous-entend la notion même de hiérarchie de classe sont particulièrement lents si on décide d'y avoir recours



    Je tiens cependant à le répéter encore une fois : la solution n'est pas mauvaise, étant donnée qu'elle est correcte, qu'elle fournit le résultat que nous espérons.

    Simplement, cette solution est "moins efficace" dans le sens où elle met en place des mécanismes qui ... dépensent inutilement de précieuses micro secondes dans un contexte dans lequel on fait justement tout pour les économiser.

    Entièrement ok avec toi sur ce point aussi. Je dois encore réfléchir à comment gérer mes entités avec ce nouveau schéma. On peut imaginer toute sorte de chose mais dans tous les cas, la description que tu fais ici restera valable dans tous les cas...
    sur ce point, la solution la plus simple est toujours la meilleure...

    La notion d'entité représente -- d'abord et avant tout la notion
    d'une chose identifiable (le plus rapidement possible) sans la moindre ambiguïté et sans risque d'erreur
    Le C++ nous propose, justement, plusieurs types de donnée particulièrement cool qui supporte à la fois un grand nombre de valeurs différentes et dont la comparaison se fait absolument sans le moindre risque d'erreur. Je pense bien sur aux types de données permettant la représentation de valeur numériques entières (char, short, int, long ou long long), que nous serions bien inspirés d'utiliser dans leur version non signée.

    Pourquoi n'utiliserais tu pas l'un de ces types particuliers pour représenter ta notion d'entité
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  11. #11
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Merci beaucoup pour ce retour très instructif une fois encore.

    Je savais que les fonctions virtuelles impliquaient la création de "vtables", mais je n'imaginais pas qu'une perte de performance était à déplorer (ou du moins, pas au point d'être suffisante pour être prise en compte, même dans le cas où l'on recherche une performance accrue). Je vais donc de ce pas m'interdire autant que ce peut, au moins dans les classes "impliquées" dans la boucle principale, l'utilisation de telles fonctions.

    Poses toi la question inverse : pourquoi devrais-tu payer le surcoût issu d'une fonction virtuelle si c'est pour ne pas en tirer profit en regroupant les différentes instances dans un container "de pointeurs sur la classe de base
    En fait, pour ce point, j'hésite encore dans le sens où cela m'aiderait tout de même beaucoup d'avoir ces systèmes dans un container unique. Certes, je pense pouvoir être capable de trouver une solution à ce problème, mais, dans le cadre d'un prototype (le temps que je gagne en assurance pour pouvoir créer un container digne de ce nom pour ces systèmes), tout en ayant conscience maintenant que la virtualité des fonctions a un impact, je pense me permettre de de créer un tel héritage tout en conservant l'objectif de le supprimer par la suite.
    Mais.... Effectivement.... Je vais encore y réfléchir : ne vaut-il pas mieux mettre les mains dans le cambouis maintenant et partir sur une base relativement propre.


    Le C++ nous propose, justement, plusieurs types de donnée particulièrement cool qui supporte à la fois un grand nombre de valeurs différentes et dont la comparaison se fait absolument sans le moindre risque d'erreur. Je pense bien sur aux types de données permettant la représentation de valeur numériques entières (char, short, int, long ou long long), que nous serions bien inspirés d'utiliser dans leur version non signée.

    Pourquoi n'utiliserais tu pas l'un de ces types particuliers pour représenter ta notion d'entité
    Oui, c'est tout à fait ce que je compte faire ; autant que possible.

    J'avais déjà jeté un œil au principe SOLID que tu rappelles ici. Je pense désormais y voir un peu plus clair au travers de ces lois. Je vais m'exercer à les appliquer autant que possible, d'autant plus que ces dernières (et je pense que c'est finalement l'objectif de ces règles) me parlent beaucoup quand à la manière dont je souhaite pouvoir aborder la programmation.

    Enfin, une question concernant les méthodes virtuelles et héritage ; qu'en est-il des fonctions statiques chez les enfants d'une classe. Par exemple :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base
    {
    	void something();
    };
     
    class Child()
    {
    	void something() { /*...*/ };
    	void update() {};
    	static void otherThing() {};
    };
    Dans ce cas, est-ce que les fonctions update() et surtout otherThing() sont virtualisées ?
    Comme ça, dans mon imagination, je dirais que oui, mais.... Je préfère en avoir le cœur net.


    Merci !

  12. #12
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Pas d'héritage, pas de virtual --> pas de virtualité ?

  13. #13
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base
    {
    	void something();
    };
     
    class Child()
    {
    	void something() { /*...*/ };
    	void update() {};
    	static void otherThing() {};
    };
    Dans ce cas, est-ce que les fonctions update() et surtout otherThing() sont virtualisées ?
    Comme ça, dans mon imagination, je dirais que oui, mais.... Je préfère en avoir le cœur net.


    Merci !
    Seules les fonctions (héritées) marquées explicitement comme virtuelles sont virtuelles.

    Comme il n'y a aucun héritage ici, il n'y aura déjà aucune fonction virtuelle ici. Et même si on modifiait un tout petit peu le code pour lui donner la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Base
    {
    public:
    	void something(){
                std::cout<<"something() from Base\n";
            }
    };
     
    class Child : public Base
    {
    public:
    	void something() {
                std::cout<<"something() from Child\n";
            };
    	void update() {};
    	static void otherThing() {};
    };
    something ne serait toujours pas virtuelle (parce que la fonction something() de la classe Base n'est pas déclarée comme virtuelle), ce qui aurait un effet "assez surprenant" dans le sens où l'on s'attendrait, avec un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    /* l'idéal serait toujours d'utiliser les pointeurs intelligents, mais, ce n'est pas le problème ici */
    Base* obj=new Child;
    obj->something();
    /* ... */
    on s'attendrait à ce que l'exécution provoque l'affichage de something() from Child. Mais, comme la fonction n'est pas virtuelle et que le compilateur connaît obj comme étant (un pointeur sur) un objet de type Base, l'affichage prendra la forme de something() from Base.

    Pour obtenir le résutat souhaité, nous devrions avoir recours à un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    static_cast<Child*>(obj)->something(); //ou dynamic_cast, si on n'est pas sur du type réel de obj
    La raison en est que la fonction void Child::something() ne fait que cacher la fonction void Base::something(), au lieu de mettre en place le mécanisme polymorphe

    Quant à otherThing(), vu que c'est une fonction statique et qu'une fonction statique ne peut pas être virtuelle, quoi que tu fasse, la messe est dite, et le compilateur t'engueulera si tu essaye de briser cette règle

    Note que c'est un mécanisme spécifique à C++ : en java, par exemple, toutes les fonctions non statiques sont forcément virtuelles, si bien que something() et update seraient forcément considérée comme étant virtuelles (mais la philosophie java est totalement différente de la philosophie C++ )

    Enfin, il est peut-être intéressant de comprendre la manière de fonctionner du compilateur lorsqu'il est confronté à une fonction membre: il se contente de rajouter implicitement un pointeur (nommé this) sur le type de la classe comme premier paramètre de la fonction.

    Comme tu sais, normalement, qu'il n'y a aucune différence (en dehors de l'accessibilité par défaut) entre le mot clé class et le mot clé struct (du moins, en C++, car d'autres langages auront d'autres règle ), imaginons donc une structure proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct Truc{
        int x;
        int y;
        void move(int diffx, int diffy);
        Truc(int x, int y);
    };
    void Truc::move(int diffx, int diffy){
        x+=diffx;
        y+=diffy;
    }
    Truc::Truc(int x, int y):x{x}, y{y}{
     
    }
    et une structure proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct Machin{
        int x;
        int y;
    };
    void Machin_move(Machin * self, int diffx, int diffy){
        self->x+= diffx;
        self->y+= diffy;
    };
    Machin Machin_create(int x, int y){
        Machin temp;
        temp.x=x;
        temp.y=y;
        return temp;
    }
    La première chose, c'est que sizeof(Truc) renverra la même valeur que sizeof(Machin), qui correspondra à la somme de la taille des différentes données qui composent la structure (+ quelques bits d'alignement éventuels), ce qui démontre clairement qu'aucune fonction membre ne fait réellement partie de la classe (ou de la structure)

    La deuxième chose, qu'il faut savoir, c'est que, si je demande au compilateur de générer le code assembleur, je vais obtenir
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    / la fonction move
            .globl  _ZN4Truc4moveEii
        .type   _ZN4Truc4moveEii, @function
    _ZN4Truc4moveEii:
    .LFB0:
        .cfi_startproc
        addl    %esi, (%rdi)
        addl    %edx, 4(%rdi)
        ret
        .cfi_endproc
    pour le fonction void Truc::move et
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // la fonction move
        .globl  _Z11Machin_moveP6Machinii
        .type   _Z11Machin_moveP6Machinii, @function
    _Z11Machin_moveP6Machinii:
    .LFB4:
        .cfi_startproc
        addl    %esi, (%rdi)
        addl    %edx, 4(%rdi)
        ret
        .cfi_endproc
    pour la fonction void Machin_moveAvoue que c'est confondant de similitude, non

    Mieux encore, si je décidais d'utiliser ces fonctionnalités dans un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int main(){
        Truc  t{3,5};
        t.move(5,10);
        std::cout<<"t.x: "<<t.x<<" t.y: "<<t.y<<"\n";
        Machin m=Machin_create(3,5);
        Machin_move(&m, 5,10);
        std::cout<<"m.x: "<<m.x<<" m.y: "<<m.y<<"\n";
    }
    et que j'en faisais générer le code assembleur, j'obtiendrais, pour l'utilisation de truc:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    	movl	$5, %edx       //valeur du deuxième paramètre du constructeur
    	movl	$3, %esi       // valeur du premier paramètre du constructeur
    	movq	%rax, %rdi     //adresse de l'élément dans la stack
    	call	_ZN4TrucC1Eii  //appel du constructeur
    	leaq	-8(%rbp), %rax // adresse effective de t dans la stack
    	movl	$10, %edx      //valeur du troisième paramètre (diffY) de la fonction move (le deuxième, en réalité, vu que this a été ajouté)
    	movl	$5, %esi       //valeur du deuxième paramètre (diffX) de la fonction move (le premier, en réalité, vu que this a été ajouté)
    	movq	%rax, %rdi     // valeur du premier paramètre (this) de la fonction move (le pointeur ajouté automatiquement par le compilateur)
    	call	_ZN4Truc4moveEii // appel de la fonction Truc::Move
    et pour l'utilisation de Machin, j'obtiendrais
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    	movl	$5, %esi            // valeur du deuxième paramètre de la fonction Machin_create
    	movl	$3, %edi            // valeur du premier paramètre de la fonction Machin_create
    	call	_Z13Machin_createii // appel de la fonction Machin_create
    	movq	%rax, -16(%rbp)     // déplacement du résultat renvoyé dans la stack
    	leaq	-16(%rbp), %rax     // récupération de l'adresse effective de m
    	movl	$10, %edx           // valeur du troisième paramètre de Machin_move (diffY)
    	movl	$5, %esi            // valeur du deuxième paramètre de la fonction Machin_move (diffX)
    	movq	%rax, %rdi        // valeur du premier paramètre de la fonciton Machin_move (le pointeur "self")
    	call	_Z11Machin_moveP6Machinii // appel de la fonction
    Hormis l'inversion entre
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    	movq	%rax, %rdi    //adresse de l'élément dans la stack
    	call	_ZN4TrucC1Eii     //appel du constructeur
    Hormis l'inversion entre
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    	movq	%rax, %rdi     //adresse de l'élément dans la stack
    	call	_ZN4TrucC1Eii  //appel du constructeur
    et
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    	call	_Z13Machin_createii // appel de la fonction Machin_create
    	movq	%rax, -16(%rbp)     // déplacement du résultat renvoyé dans la stack
    (et, bien sur, l'adaptation des adresse lorsque l'on fait appel à la stack), tu avouera que le résultat est, là encore, vachement identique, non

    Retour sur le (b) : "la POO là où elle rends le code plus naturel"

    Enfin, il faut savoir que la seule différence entre une fonction membre normale (non statique) et une fonction membre statique tient dans le fait que la fonction membre statique ne dépend d'aucune instance particulière de la classe à laquelle elle appartient, avec comme résultat que le compilateur:
    1. veillera à ce que l'on utilise le nom pleinement qualifié de la fonction statique (ex: MaClasse::laFonctionStatique)
    2. supprimera sans doute les appels à leaq -8(%rbp), %rax // adresse effective de t dans la stack et à movq %rax, %rdi // valeur du premier paramètre (this) de la fonction move (le pointeur ajouté automatiquement par le compilateur) lors de leur appel

    Note que, en dehors du nom pleinement qualifié (que l'on peut obtenir d'une autre manière, comme en créant une fonction libre dans un espace de nom) propre à une fonction statique, il n'y a donc absolument aucune différence entre une fonction membre statique et une fonction libre

    Au final, si on cherchait vraiment à économiser la moindre micro seconde, on aurait intérêt à préférer l'utilisation d'une fonction membre statique à celle d'une fonction membre "normale" (non statique), pour autant, bien sur, que l'on ne décide pas de transmettre l'équivalent du paramètre this de manière explicite

    Par contre, il n'y a absolument aucune différence entre l'appel que l'on fait à une fonction membre statique et l'appel que l'on peut faire à une fonction libre (qu'elle soit ou non dans un espace de noms ne changera rien )

    La vrai question que l'on devrait donc se poser lorsque l'on envisage l'utilisation d'une fonction statique est donc proche de
    aurai-je vraiment besoin de créer une instance particulière de la classe en question à un moment quelconque
    Si la notion représentée par ta classe correspond effectivement à ce que l'on peut associer à un type de donnée bien particulier, que l'on envisage d'utiliser pour définir des données bien particulières à différents endroits du code(ex: une structure ou une classe Point ou FormeGeometrique), on a sans doute de bonnes raisons de préférer l'utilisation d'une fonction membre statique.

    Mais si on n'envisage de créer une classe que pour lui fournir des fonctions membres statiques (et aucune fonction membre "normale"), parce que l'on n'aura jamais besoin d'une instance bien particulière de la classe en tant que telle, on aura sans doute intérêt à préférer l'utilisation de fonction libres, éventuellement regroupées au sein d'un espace de noms clairement défini
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  14. #14
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Merci beaucoup pour ce retour.

    oui, pour le code présenté, j'avais omis le petit détail qui, forcément, t'a sauté aux yeux. Mais l'idée est bien celle-ci.
    Bon, à savoir que finalement, tout ce beau monde se ressemble tant après la compilation (au détail près des hash et des noms).

    Donc finalement, si je récapitule, le fait de faire "hériter" (au sens léger, sans déclaration virtuelle des fonctions parentes), n'impacte alors finalement pas les performances à l’exécution, mais cet héritage ne sera finalement d'aucune utilité. Je comprends mieux...


    Justement, concernant mes "managers" qui pilotent tout ce beau monde, pour le moment, la simplicité du code le permettant, toutes ces fonctions sont regroupées dans un espace de nom uniquement plutôt qu'une classe. ceci me simplifie pas mal l'implémentation. Néanmoins, il deviens alors primordial de proscrire l'utilisation des using namespace. Ceci me va bien dans le sens où, personnellement, j'apprécie conserver le détail des espaces de noms, mais pour d'autres, il faut porter une attention particulière.

  15. #15
    Expert éminent sénior
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Analyste/ Programmeur

    Informations forums :
    Inscription : Juillet 2013
    Messages : 4 629
    Points : 10 554
    Points
    10 554
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Note que, en dehors du nom pleinement qualifié (que l'on peut obtenir d'une autre manière, comme en créant une fonction libre dans un espace de nom) propre à une fonction statique, il n'y a donc absolument aucune différence entre une fonction membre statique et une fonction libre
    à la notion de friend près, parce qu'une est membre (<- et un coup Google plus tard, cette méthode peut utiliser tous les membres de la classe, statiques ou pas (avec 1 instance de la classe))

  16. #16
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par foetus Voir le message
    à la notion de friend près, parce qu'une est membre (<- et un coup Google plus tard, cette méthode peut utiliser tous les membres de la classe, statiques ou pas (avec 1 instance de la classe))
    Tout à fait, mais, d'un autre côté...

    L'utilité d'une fonction membre statique placée dans une accessibilité restreinte (seule raison pour laquelle l'amitié aurait un sen) reste malgré tout à démontrer. Et, surtout, comme on peut -- effectivement -- trouver de bonnes raisons d'avoir de telles fonctions -- parce qu'elles correspondraient à des détails d'implémentation, par exemple -- l'utilité d'une déclaration d'amitié dans un tel cas devient beaucoup plus difficile à défendre

    Car, si on y réfléchit un tout petit peu : une fonction membre statique est -- par nature -- soumise à une restriction importante, vu qu'elle ne dépend d'aucune instance particulier : elle ne s'applique -- a priori -- à aucune instance en particulier de la classe.

    Trouver un cas d'utilisation pour lequel une fonction qui ne s'applique à aucune instance en particulier de la classe puisse être considéré comme un détail d'implémentation devient donc malgré tout relativement difficile

    Et, quand bien même, si nous arrivons à trouver un tel cas d'utilisation, il faudrait encore trouver un cas d'utilisation qui puisse en dépendre mais qui ne fasse pas partie des services que l'on est en droit d'attendre de la classe en elle-même pour justifier le recours à l'amitié... Ca fait quand même pas mal de restrictions, non

    Alors, bien sur, j'ai bien conscience que l'on ne peut jamais être sur de rien, qu'il y aura toujours des situations qui font que, qu'on peut toujours trouver des exceptions. Mais je crois qu'un minimum de pragmatisme est aussi parfois utile non
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  17. #17
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Justement, concernant mes "managers" qui pilotent tout ce beau monde, pour le moment, la simplicité du code le permettant, toutes ces fonctions sont regroupées dans un espace de nom uniquement plutôt qu'une classe. ceci me simplifie pas mal l'implémentation. Néanmoins, il deviens alors primordial de proscrire l'utilisation des using namespace. Ceci me va bien dans le sens où, personnellement, j'apprécie conserver le détail des espaces de noms, mais pour d'autres, il faut porter une attention particulière.
    Ah, mais, de toutes manières, cela fait au moins quinze ans qu'on engueule de manière systématique ceux qui utilise la directive using namespace

    Et cela fait déjà un sérieux bout de temps que j'essaye de sensibiliser les gens aux raisons qui incitent à utiliser les espaces de noms
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  18. #18
    Expert éminent sénior
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Analyste/ Programmeur

    Informations forums :
    Inscription : Juillet 2013
    Messages : 4 629
    Points : 10 554
    Points
    10 554
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Trouver un cas d'utilisation pour lequel une fonction qui ne s'applique à aucune instance en particulier de la classe puisse être considéré comme un détail d'implémentation devient donc malgré tout relativement difficile
    Je suis moins catégorique que toi C'est vrai que les méthodes (fonctions membres) statiques ont, par rapport aux fonctions libres amies, la notion de sémantique (elles sont liées à la classe).
    Mais tu peux les utiliser pour :

    *) La création/ destruction d'instances. Par exemple, tu peux coder avec les patrons de conception fabrique ("factory") et monteur ("builder"). Je pense que leur avantage par rapport au couple constructeur/ destructeur, c'est l'uniformisation.
    *) Des opérations avec au moins 2 instances. Par exemple (exemple non pertinent), tu peux coder pour ta classe Matrice une addition à la puissance X (ne me demande pas à quoi cela peut servir ) : Matrix::myop(A, B, 5); // (A + B) ^ 5

  19. #19
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par foetus Voir le message
    Je suis moins catégorique que toi C'est vrai que les méthodes (fonctions membres) statiques ont, par rapport aux fonctions libres amies, la notion de sémantique (elles sont liées à la classe).
    Mais tu peux les utiliser pour :

    *) La création/ destruction d'instances. Par exemple, tu peux coder avec les patrons de conception fabrique ("factory") et monteur ("builder"). Je pense que leur avantage par rapport au couple constructeur/ destructeur, c'est l'uniformisation.
    A ceci près que tu n'as -- a priori -- aucune raison d'utiliser une membre statique de la classe que tu fabriques Et encore moins d'utiliser une fonction membre statique dont l'accessibilité a été restreinte
    Citation Envoyé par foetus Voir le message
    *) Des opérations avec au moins 2 instances. Par exemple (exemple non pertinent), tu peux coder pour ta classe Matrice une addition à la puissance X (ne me demande pas à quoi cela peut servir ) : Matrix::myop(A, B, 5); // (A + B) ^ 5
    Mais, encore une fois, pourquoi voudrais tu restreindre l'accessibilité de cette fonction, alors qu'il semble évident qu'elle correspond clairement à l'un des services que tu estimes être en droit d'attendre de la part de ta classe

    Tu peux tourner les choses dans tous les sens: une fonction statique peut s'avérer utile / intéressante, tout comme une fonction privée / protégée, je ne remet ni l'un ni l'autre en question. Mais une fonction statique ET protégée / privée, cela ne me semble pas vraiment très logique
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  20. #20
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Bon, merci pour ces précisions / discussions fort intéressantes.

    J'ai pu réalisé une première implémentation fonctionnelle (pour une partie) de mon ECS. Comme annoncé, tous les "managers" sont regroupés dans des namespace plutôt que dans des classes, et j'essaie d'appliquer autant que possible les principes "SOLID" que j'affectionne déjà.

    Globalement, voici comment ça se déroule.

    * on créé une entité (simple size_t)
    * on bind les des composants divers à ces entités. Les composants sont stockés dans un container indexé générique : component::bind<Position>(id, params...);* a chaque bind d'un composant à une entité, on met à jour les systèmes (j'y reviens après) avec cette entité
    * Compte tenu de l'utilisation attendue, pour le moment, la forme retenue reste d'avoir deux classes pour les systèmes :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    template<typename ...ComponentTypes>
    class System
    {
    protected:
     
    	// ivector est ici un vecteur indexé
    	ivector<entity_t> m_entities;
     
    public:
     
    	void updateSystem(entity_t const & cEntity);
     
    	virtual void execute(float const &)=0;
     
    };
     
     
    class Move: public System<Position, Velocity>
    {
    public:
     
    	virtual void execute(float const &) override final;
     
    };

    En fait, si je me suis permis de conserver un héritage tel-quel, c'est pour me permettre de facilement créer de nouveaux systèmes sans vraiment avoir à me soucier de la manière dont ils sont mis à jour. En effet, la méthode updateSystem(entity_t const &) permet de stocker dans m_entities uniquement les entités qui seront impactées par le-dit système.
    Je cherche justement, actuellement, à voir comment je peux me passer de ça. Pour le moment, cette solution me permet d’écrire dans les systèmes :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    virtual void execute(float const &) override final
    {
    	for(auto e:m_entities)
    	{
    		auto pos = &component::get<Position>(e);
    		auto vel = &component::get<Velocity>(e);
     
    		pos->x += vel->x;
    		pos->y += vel->y;
    		// ...
    	}
    }

    Comme on le voit ici, je n'ai pas à me soucier, dans la boucle principale, de savoir si l'entité en cours de traitement possède ou non le composant souhaité.


    Tout ça est bien beau, mais je dois trouver une réponse aux questions suivantes :
    * quelle autre solution ais-je pour éviter d'avoir à réaliser cet héritage, tout en me permettant de filtrer uniquement sur les entités concernées par les composants souhaités ?
    * a la destruction d'une entité, comment m'assurer que tous les composants seront détruits aussi sachant que mon container est de la forme :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    template<typename ComponentType>
    ivector<ComponentType> getComponent() {
    	static ivector<ComponentType> cmp;
    	return cmp;
    }
    * enfin, mais ceci viendra une fois que j'aurais répondu aux deux premières questions, quelle est la meilleure solution pour gérer les évènements ? J'avais bien fait une implémentation très rapide de signaux / slots, mais cette dernière implémentation ne me permet pas encore de gérer efficacement toutes les formes de fonctions souhaitées, et n'est pas du tout thread-safe.


    Si vous avez des idées et/ou des orientations qui pourraient m'être utiles, je vous en serais très reconnaissant.





    Le temps que je réponde aux 3 questions ci-dessous, je réalise quelques tests, et voici ce qu'il en ressort :
    - performances encore relativement faibles. Je tourne entre 21 et 75 FPS pour l'affichage de 128'000 particules selon la construction des composants (voir second point) ce qui me parait, une fois encore, ridicule. A voir si cela est le résultat de la "virtualisation" des systèmes, mais je pense, dans un premier temps, qu'il y a plus à gagner en grattant ailleurs... Je dois trouver où. Serai-ce SFML qui bride les performances ? J'en doute.
    - la manière de structurer les composants est extrêmement importante. Pour 128'000 particules je tourne à 21 FPS dans le cas où les composants Position, Vitesse, Rotation et Renderable sont distincts. Par contre, si je fait de tout ça un seul composant, j'affiche 75 FPS.

    Tout ceci m’amène à me poser la question suivante : mon implémentation est-elle réellement cache-friendly ? Je vais essayer de réaliser le même exercice avec un programme test qui ne permet que d'afficher des particules et où les "composants" sont traités de manière directe par le système (sont intégrés au système), juste pour voir si les sujets de performance sont uniquement dues à mon implémentation.

+ Répondre à la discussion
Cette discussion est résolue.
Page 1 sur 3 123 DernièreDernière

Discussions similaires

  1. Héritage initialisation membre const
    Par themadmax dans le forum C++
    Réponses: 7
    Dernier message: 26/05/2011, 15h31
  2. Pointeur de fonction membre et héritage
    Par Caduchon dans le forum Langage
    Réponses: 6
    Dernier message: 25/03/2011, 12h02
  3. [POO] Héritage : Surcharge d'un membre statique parent
    Par Nullos Oracle dans le forum Langage
    Réponses: 7
    Dernier message: 11/09/2007, 18h39
  4. Réponses: 16
    Dernier message: 17/03/2007, 17h31
  5. [POO] Pointeur sur fonction membre et héritage
    Par MrDuChnok dans le forum C++
    Réponses: 9
    Dernier message: 20/07/2006, 17h19

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo