Conception d'une classe de signaux

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Hello !

Voilà, je me suis lancé dans la conception d'une classe de signaux (style signaux/slots) simpliste en C++11 mais ne demandant qu'à évoluer, concrètement voici où j'en suis:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template<typename... Args>
class NzSignal
{
    public:
        using Callback = std::function<void(Args...)>;

        NzSignal() = default;
        ~NzSignal() = default;

        void Connect(const Callback& func);
        void Connect(Callback&& func);
        template<typename O> void Connect(O& object, void (O::*method)(Args...));
        template<typename O> void Connect(O* object, void (O::*method)(Args...));

        void operator()(Args&&... args);

    private:
        std::vector<Callback> m_callbacks;
};
 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
template<typename... Args>
void NzSignal<Args...>::Connect(const Callback& func)
{
    m_callbacks.push_back(func);
}

template<typename... Args>
void NzSignal<Args...>::Connect(Callback&& func)
{
    m_callbacks.emplace_back(std::move(func));
}

template<typename... Args>
template<typename O>
void NzSignal<Args...>::Connect(O& object, void (O::*method) (Args...))
{
    return Connect([&object, method] (Args&&... args)
    {
        return (object .* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
template<typename O>
void NzSignal<Args...>::Connect(O* object, void (O::*method)(Args...))
{
    return Connect([object, method] (Args&&... args)
    {
        return (object ->* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
void NzSignal<Args...>::operator()(Args&&... args)
{
    for (const Callback& func : m_callbacks)
        func(std::forward<Args>(args)...);
}

Soit une implémentation extrêmement simpliste ne manquant fondamentalement que d'une chose: le support de la déconnexion.
Pour ce faire, je souhaiterai que ma méthode fonction membre Connect renvoie un objet du style "Connection", un objet représentant donc très simplement le lien entre le signal et le slot. L'objectif est ensuite de disposer d'un objet ScopedConnection me permettant d'utiliser le RRID (Resource Release Is Destruction, un nom que je préfère à RAII).

Bien entendu, cette interface est très inspirée de celle de Boost, que je n'utilise pas pour ses interdépendances et sa lourdeur (ne serait-ce qu'au niveau de la compilation), ce point a déjà été largement discuté, ce sujet n'est pas là pour ça.

Mon problème c'est que je n'ai aucune idée de comment procéder à l'implémentation, je ne suis pas très familier avec l'utilisation poussée des templates, et ici je ne sais vraiment pas comment je suis censé faire le lien entre un objet Connection (pouvant être copié), un objet Signal template et un accès aux callbacks (pour les identifier).

Mon objectif est que cette classe soit performante surtout au niveau du signalement, étant donné qu'elle risque d'être utilisée à des endroits un minimum critiques de mon moteur.

Notez bien qu'ici je ne demande pas forcément de solution toute faite, tout au plus des pistes de réflexion :)

En vous remerciant !

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

Stocke tes éléments dans une liste, renvoie un itérateur sur l'élément inséré et la deconnection est juste un erase de l'itérateur.

Après, tu perds un peu performance vis à vis d'un vecteur (déréférencement supplémentaires, localité du cache toute pourie..) A toi de benchmarker pour voir si c'est grave.

+0 -0

Salut,

Même si tu ne prévois pas d'utiliser boost, ça peut quand même être vrachement intéressant de regarder comment c'est fai. JE n'ai pas l'impression que boost::signal2 soit si lourd que ça, mais bon. Le seul truc con c'est qu'ils ont fait leur propre type boost::function au lieu d'utiliser std::function.

Ma plateforme avec 23 jeux de société classiques en 6 langues et 13000 joueurs: http://qcsalon.net/ | Apprenez à faire des sites web accessibles http://www.openweb.eu.org/

+0 -0
Auteur du sujet

Stocke tes éléments dans une liste, renvoie un itérateur sur l'élément inséré et la deconnection est juste un erase de l'itérateur.

Davidbrcz

Le problème vient alors de l'utilisation de plusieurs objets Connection, que se passe-t-il lors de la déconnexion ? Comment invalider les autres Connections ?
Plus j'y réfléchis et plus je me dis que je vais être obligé d'utiliser l'allocation dynamique et un shared_ptr (voire un weak_ptr), ce qui m'emmerde au niveau performances du coup.

Salut,

Même si tu ne prévois pas d'utiliser boost, ça peut quand même être vrachement intéressant de regarder comment c'est fai. JE n'ai pas l'impression que boost::signal2 soit si lourd que ça, mais bon. Le seul truc con c'est qu'ils ont fait leur propre type boost::function au lieu d'utiliser std::function.

QuentinC

J'ai un peu regardé, ça passe par des pointeurs void et un pointeur de fonction prenant deux void en argument, je ne suis pas convaincu que ça soit la meilleure solution (ni que leur implémentation date du C++11) donc j'en cherche de meilleures.

Quant à boost::function, ça existait avant std::function, tout simplement.

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

Si tu veux utiliser des signaux-slots dans du code critique, c'est assez simple : ne fais pas cela :) Le rôle des signaux-slots, c'est le découplage de tes classes. Plus tu vas implémenter de fonctionnalités (connecter plusieurs slots, déconnexion automatique, etc), plus tu vas perdre en performance. Ne vise pas une seule classe de signaux-slots, mais plusieurs selon le contexte :

  • une avec 1 seule connexion
  • une avec plusieurs connexions fixes
  • une avec connexions dynamiques
  • une avec auto-déconnexion
  • une avec shared (thread safe ?)
  • etc

(bien sur, tu n'es pas obligé de créer plusieurs classes, tu peux créer une classe template avec des politiques)

AU fait, si tu veux étudier aussi comment fonctionne les signaux-slots de Qt, tu peux lire http://woboq.com/blog/how-qt-signals-slots-work.html et http://woboq.com/blog/how-qt-signals-slots-work-part2-qt5.html (je sais que tu n'aimes pas Qt. Le but n'est pas de l'utiliser, mais de voir les problématiques qu'ils ont rencontré et comment ils ont résolu. Regarde en particulier comment ils ont implémenter les connects et callback).

Il est probablement possible de jouer avec les macros et méta-prog pour détecter automatiquement si tu as plusieurs connexions différentes dans une classe (utiliser une liste de connexion plutôt qu'une connexion unique), si tu as des connexions dynamiques, etc. pour éviter de devoir choisir "manuellement" quel type de classe utiliser, mais dans un premier temps, ce n'est pas nécessaire de s'embêter je pense.

+0 -0
Auteur du sujet

Si tu veux utiliser des signaux-slots dans du code critique, c'est assez simple : ne fais pas cela :)

gbdivers

Le code n'est pas si critique que ça, c'est par exemple mon NodeComponent qui va invalider la ViewMatrix du CameraComponent en cas de déplacement par exemple, je me demande si c'est viable avec ce genre de signaux.
C'est à peu près le signal le plus critique que j'aie (si je vois que les signaux prennent trop de temps à ce niveau-là, je ferais probablement un simple callback).
Pour le reste des signaux, c'est par exemple pour notifier la recompilation d'un shader aux render techniques (pour qu'elles mettent à jour les indices des uniformes), une notification de la fenêtre en cas de changement de taille, ou la notification de la libération d'une ressource (pour libérer la mémoire dans le cache des renderqueues).

Plus tu vas implémenter de fonctionnalités (connecter plusieurs slots, déconnexion automatique, etc), plus tu vas perdre en performance. Ne vise pas une seule classe de signaux-slots, mais plusieurs selon le contexte :

  • une avec 1 seule connexion
  • une avec plusieurs connexions fixes
  • une avec connexions dynamiques
  • une avec auto-déconnexion
  • une avec shared (thread safe ?)
  • etc

(bien sur, tu n'es pas obligé de créer plusieurs classes, tu peux créer une classe template avec des politiques)

gbdivers

À voir, mais c'est intéressant comme idée. Je pense que c'est à réfléchir une fois que j'aurai une première implémentation fonctionnelle.

je sais que tu n'aimes pas Qt.

gbdivers

D'où sors-tu que je n'aime pas Qt ? :D
Au contraire, j'aime assez et j'ai même prévu de l'utiliser pour faire les outils du SDK.
(Ah peut-être de la fois où j'ai trollé sur la qualité du code).

Bref je vais regarder tes liens, et je pense me pencher sur une première implémentation à base de pointeurs intelligents, autant essayer de faire quelque chose qui fonctionne avant de l'optimiser.

Édité par Lynix

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

J'aime bien les signaux-slots, mais cela peut poser un problème conceptuel (et j'aime me prendre la tête avec les problèmes conceptuels. Voire à créer dans ma petite tête des problèmes qui n'existent pas). Basiquement, les signaux-slots permettent d'éviter les couplages forts, ie ne pas appeler directement une fonction sur objet dans un autre objet (ce qui implique de conserver l'objet a appeler, inclure les classes, avoir un code spécifique d'une fonction membre, etc).

Mais le risque est d'ajouter un couplage (faible au lieu d'un fort, mais couplage quand même) qui n'est pas forcement nécessaire.

Pour l'auto-déconnexion, on demande à l'objet qui reçoit le signal de prévenir quand il est détruit pour que l'objet qui lance le signal puisse se déconnecter.

Dans les 2 cas, on donne des responsabilités (peut être) trop importantes à certains objets, simplement parce que c'est simple de créer une connexion signal-slot (c'est particulièrement visible avec Qt, qui utilise pas mal les signaux et slots).

Du coup, pourquoi ca serait aux shaders de prévenir le render ? aux fenêtres de prévenir des changements ? des ressources de leur destruction ?

J'utilise un pipeline avec nodes et connexions (traitement vidéo), avec une classe builder qui est chargé de créer et détruire les connexions et nodes. Lorsque je change un shader, je n'accède pas directement à lui, je préviens le builder qu'il y a un changement dans l'organisation du pipeline à faire. Si j'ai besoin de détruire une ressource, je le demande au builder, qui fait le boulot correctement (en libérant aussi les nodes qui ne servent plus, en déconnecter les nodes qui n'ont plus besoin, etc).

Pour les "connexions" hors pipeline principal (ie qui ne concerne pas la vidéo, par exemple un changement de taille de fenêtre signalé au renderer), j'utilise des "monitor" et "controler", qui sont également géré (création et destruction) par le builder. Et ceux-ci utilisent directement des références ou pointeurs. Le fait de passer par le builder implique que je n'ai pas à gérer des dangling.

Pour l'ECS (je ne sais plus si on en avait déjà parlé), je préfère aussi avoir un système ultra simple pour les "interactions" inter-components (simple pointeur ou référence). Lors de la création ou destruction d'une entité, je parcours les components pour mettre à jour si nécessaire (avec des signaux-slots, chaque component préviendrait les components qui sont liés). Cette opération n'est pas faite souvent par définition (ie égal au FPS, voire moins souvent), ce n'est pas critique de faire cela (en optimisant bien sur le parcours des components, en les mettant dans un tableau contiguë). Par contre, les interactions entre components sont plus facilement critiques (appelé plusieurs centaines de fois par frame par component), il me semble plus intéressant d'avoir des interactions rapides, quitte à perdre un peu de temps pour les créations/destructions.

Bref, tout ça pour dire qu'il faut faire attention avec les signaux-slots. C'est une fonctionnalité que tu peux proposer aux utilisateurs de ton moteur (puisqu'ils ne seront peut être pas aussi exigeant que toi sur la qualité du code), mais tu devrais peut être les éviter en interne.

Édité par gbdivers

+0 -0
Auteur du sujet

Disons qu'il me faut bien un système de toute façon, voilà l'idée:

Mes RenderTechniques ont besoin de connaître les indices des uniformes pour envoyer les bonnes informations aux shaders, c'est une information qu'elles peuvent demander au shader au moment où elles en ont besoin mais c'est assez lourd à faire.
De fait, l'information est placée dans un cache, stocké par la RenderTechnique.
Maintenant, si le shader est recompilé, le cache doit être mis à jour évidemment, je pense que les signaux/slots correspondent très bien à ce cas de figure (surtout que là ce n'est pas un cas critique).

Pour l'ECS, je pourrais également faire ça (j'ai un système de callbacks au niveau des composants qui permet la notification si un autre composant est attaché/détaché), j'ai juste peur de perdre en flexibilité.
Par exemple, dans le cas de mon CameraComponent, la matrice de vue dépend directement des données du NodeComponent, de fait le CameraComponent fait en sorte d'être notifié (de connecter un signal) dès qu'un NodeComponent fait partie de la même entité.
Si je voulais faire ça autrement, il faudrait que mon NodeComponent lors d'une invalidation (déplacement/rotation/etc.) notifie automatiquement le CameraComponent (en vérifiant sa présence et en l'appelant).
Le problème avec cette solution est le manque de flexibilité, que se passe-t-il si l'utilisateur veut faire un composant ayant le même besoin ?

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0
Auteur du sujet

Voilà, j'ai implémenté un système de connexion fonctionnel:

 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
template<typename... Args>
class NzSignal
{
    public:
        using Callback = std::function<void(Args...)>;
        class Connection;
        class ConnectionGuard;

        NzSignal() = default;
        NzSignal(const NzSignal&) = delete;
        NzSignal(NzSignal&& signal);
        ~NzSignal() = default;

        void Clear();

        Connection&& Connect(const Callback& func);
        Connection&& Connect(Callback&& func);
        template<typename O> Connection&& Connect(O& object, void (O::*method)(Args...));
        template<typename O> Connection&& Connect(O* object, void (O::*method)(Args...));

        void operator()(Args&&... args);

        NzSignal& operator=(const NzSignal&) = delete;
        NzSignal& operator=(NzSignal&& signal);

    private:
        struct Slot;

        using SlotPtr = std::shared_ptr<Slot>;
        using SlotList = std::list<SlotPtr>;

        struct Slot
        {
            Slot(NzSignal* me) :
            signal(me)
            {
            }

            Callback callback;
            NzSignal* signal;
            typename SlotList::iterator it;
        };

        void Disconnect(const SlotPtr& slot);

        SlotList m_slots;
};

template<typename... Args>
class NzSignal<Args...>::Connection
{
    using BaseClass = NzSignal<Args...>;
    friend BaseClass;

    public:
        Connection(const Connection& connection) = default;
        Connection(Connection&& connection) = default;
        ~Connection() = default;

        void Disconnect();

        bool IsConnected() const;

        Connection& operator=(const Connection& connection) = default;
        Connection& operator=(Connection&& connection) = default;

    private:
        Connection(const SlotPtr& slot);

        std::weak_ptr<Slot> m_ptr;
};

template<typename... Args>
class NzSignal<Args...>::ConnectionGuard
{
    using BaseClass = NzSignal<Args...>;
    using Connection = BaseClass::Connection;

    public:
        ConnectionGuard(const Connection& connection);
        ConnectionGuard(Connection&& connection);
        ~ConnectionGuard();

        ConnectionGuard& operator=(const Connection& connection) = delete;
        ConnectionGuard& operator=(Connection&& connection) = delete;

    private:
        Connection m_connection;
};

#include <Nazara/Core/Signal.inl>
  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
template<typename... Args>
NzSignal<Args...>::NzSignal(NzSignal&& signal)
{
    operator=(std::move(signal));
}

template<typename... Args>
void NzSignal<Args...>::Clear()
{
    m_slots.clear();
}

template<typename... Args>
typename NzSignal<Args...>::Connection&& NzSignal<Args...>::Connect(const Callback& func)
{
    return Connect(std::move(Callback(func)));
}

template<typename... Args>
typename NzSignal<Args...>::Connection&& NzSignal<Args...>::Connect(Callback&& func)
{
    auto tempPtr = std::make_shared<Slot>(this);
    tempPtr->callback = std::move(func);

    m_slots.emplace_back(std::move(tempPtr));

    const auto& slotPtr = m_slots.back();
    slotPtr->it = m_slots.end();
    --slotPtr->it;

    return Connection(slotPtr);
}

template<typename... Args>
template<typename O>
typename NzSignal<Args...>::Connection&& NzSignal<Args...>::Connect(O& object, void (O::*method) (Args...))
{
    return Connect([&object, method] (Args&&... args)
    {
        return (object .* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
template<typename O>
typename NzSignal<Args...>::Connection&& NzSignal<Args...>::Connect(O* object, void (O::*method)(Args...))
{
    return Connect([object, method] (Args&&... args)
    {
        return (object ->* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
void NzSignal<Args...>::operator()(Args&&... args)
{
    for (const SlotPtr& slot : m_slots)
        slot->callback(std::forward<Args>(args)...);
}

template<typename... Args>
NzSignal<Args...>& NzSignal<Args...>::operator=(NzSignal&& signal)
{
    m_slots = std::move(signal.m_slots);

    // We need to update the signal pointer inside of each slot
    for (SlotPtr& slot : m_slots)
        slot->signal = this;

    return *this;
}

template<typename... Args>
void NzSignal<Args...>::Disconnect(const SlotPtr& slot)
{
    m_slots.erase(slot->it);
}


template<typename... Args>
NzSignal<Args...>::Connection::Connection(const SlotPtr& slot) :
m_ptr(slot)
{
}

template<typename... Args>
void NzSignal<Args...>::Connection::Disconnect()
{
    if (SlotPtr ptr = m_ptr.lock())
        ptr->signal->Disconnect(ptr);
}

template<typename... Args>
bool NzSignal<Args...>::Connection::IsConnected() const
{
    return !m_ptr.expired();
}


template<typename... Args>
NzSignal<Args...>::ConnectionGuard::ConnectionGuard(const Connection& connection) :
m_connection(connection)
{
}

template<typename... Args>
NzSignal<Args...>::ConnectionGuard::ConnectionGuard(Connection&& connection) :
m_connection(std::move(connection))
{
}

template<typename... Args>
NzSignal<Args...>::ConnectionGuard::~ConnectionGuard()
{
    m_connection.Disconnect();
}

D'après mes tests ça fonctionne sans problème, reste à voir ce qui est améliorable (j'aurais bien aimé avoir un std::vector à la place d'un std::list par exemple, je suis en train de me pencher dessus).
Aussi, je souhaiterai faire un Combiner pour savoir comment gérer l'éventuel retour, le type void m'emmerde parce que je ne peux évidemment pas l'instancier, et là on arrive dans la spécialisation partielle de template, qui est loin d'être mon domaine préféré :D

Enfin bon à priori rien de compliqué, je poste le code dans l'espoir d'avoir des critiques ou des suggestions !

Edit: Et voilà pour std::vector
Edit²: Oops, avec std::vector je ne peux plus déconnecter des signaux pendant l'itération sans en louper quelques uns, j'envisage du coup un booléen indiquant l'appel et un second vector contenant les indices des connexions à enlever qui serait rempli par Disconnect si appelé pendant l'itération, ça complexifie un peu mais ça me permet de garder un tableau plutôt qu'une liste du coup.

Édité par Lynix

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0
Auteur du sujet

Gaffe au retour de rvalue: se sont des temporaires sur variable locale.

jo_link_noir

Oui je vais la virer.

Il manque le const pour operator().

jo_link_noir

C'est vrai que la fonction pourrait être const, au prix de quelques éléments mutables (vu ce que je vais rajouter pour gérer les déconnexions en milieu de callback).

En ce moment je suis sur un problème de méthode virtuelle sur une autre classe qui n'override pas la classe de base, si quelqu'un a une idée de pourquoi ça peut arriver (dans les cas généraux), je deviens fou.

Édité par Lynix

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

En ce moment je suis sur un problème de méthode virtuelle sur une autre classe qui n'override pas la classe de base, si quelqu'un a une idée de pourquoi ça peut arriver (dans les cas généraux), je deviens fou.

Lynix

  • Copie d'instance vers une classe mère.
  • Mauvaise surcharge (utilise le mot clef override pour être sûr, et compile aussi avec -Woverloaded-virtual).
  • Magie noire :)

J'y pense, où sont les tests pour Nazara ?

+0 -0
Auteur du sujet

Ici :D

J'ai veillé jusqu'à quatre heures du matin pour mieux comprendre le problème, apparemment ce n'est pas directement un problème de polymorphisme mais plutôt un problème de tableau, ou de pointeur, ça fait un peu comme ça:

  1. Affectation d'un NodeComponent à l'index #0
  2. Affectation d'un CameraComponent à l'index #4
  3. Appel de m_components[0]->OnComponentAttached();
  4. CameraComponent::OnComponentAttached est appelé

wtf

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

HS : Peux tu me passer un path vers ton ECS sur ton repo de Nazara, parce que je n'arrive pas à m'y retrouver, avec tous ces fichiers ;) J'aimerais y jeter un coup d'oeil au cas où il y aurait des trucs bon à prendre pour le mien, où même pour te donner mon avis ou des conseils sur la chose, même si c'est probable moins probable au vu de la différence de niveaux entre nous :)

Édité par mehdidou99

Plus on apprend, et, euh… Plus on apprend. | Coliru, parfait pour tester ses codes sources !

+0 -0

Ok, je n'hésiterai pas. (je profite de ce post pour te féliciter de ton moteur car je ne l'avais pas encore fait : c'est très impressionant pour un "amateur" -solitaire qui plus est-. A ce que j'ai vu niveau rendu graphique par exemple, il se compare aisément au moteur Source, qui était un des meilleurs moteurs il y a à peine quelques années. Donc +10000 pouces verts pour ton moteur :)(dommage, je ne peux en mettre qu'un))

EDIT : AU passage, merci pour l'url, c'était quand même l'objectif du message ;)

EDIT 2 : Wow :waw: Je n'imaginais pas un truc aussi gros (mon ECS s'appelle liteECS++, je te laisse deviner pourquoi lite ;) ) Je me lance dans la lecture du code, et, si ça t'intéresse, je te ferai un résumé de ce que j'en pense. (je n'ai pas énormément d'expérience dans le domaine, mais je me suis documenté pendant des mois avant de me lancer dans le code, donc j'ai vu quelques trucs sympas ;) )

Édité par mehdidou99

Plus on apprend, et, euh… Plus on apprend. | Coliru, parfait pour tester ses codes sources !

+0 -0
Auteur du sujet

Merci pour tes encouragements, ça me fait plaisir !
Je n'ai pas encore dépassé le stade du moteur Source (d'un point de vue rendu), mais il est vrai que je m'en rapproche :)

EDIT 2 : Wow :waw: Je n'imaginais pas un truc aussi gros (mon ECS s'appelle liteECS++, je te laisse deviner pourquoi lite ;) ) Je me lance dans la lecture du code, et, si ça t'intéresse, je te ferai un résumé de ce que j'en pense. (je n'ai pas énormément d'expérience dans le domaine, mais je me suis documenté pendant des mois avant de me lancer dans le code, donc j'ai vu quelques trucs sympas ;) )

mehdidou99

Gros ? :D Tu parles du SDK là ? Parce qu'il est encore léger (à peine 5 000 lignes à tout casser, soit juste 1/15ème du moteur), pour la simple raison qu'il manque encore pas mal de composants.

Tout les conseils sont bons à prendre pour ma part donc n'hésite vraiment pas ;)

Édité par Lynix

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0
Auteur du sujet

Voilà, je viens de terminer ma modification du système de signaux, il permet maintenant de déconnecter n'importe quel slot, même pendant l'itération.
Pour ce faire, les callbacks sont réorganisés, par exemple:

1
2
3
4
5
6
7
8
9
 Callbacks:
 - 1
 - 2
 - 3 
 - 4
 - 5
 - 6
 - 7
 - 9

Pour supprimer un slot, je déplace le dernier slot vers celui-là et je pop le dernier élément (de la sorte, les autres slots restent bien à leurs indices), donc par exemple si je veux supprimer le quatrième slot, je déplace le neuvième à sa position et j'enlève le dernier élément.

Mais si je venais à appliquer ce processus pendant l'itération avec un slot antérieur à celui déjà parcouru, ça ferait en sorte que le dernier callback de la liste, qui se retrouverait placé avant l'itérateur, ne serait pas appelé.

Pour remédier à ça, j'ai imaginé un petit algorithme, qui va déplacer le slot actuel vers celui à supprimer, déplacer le dernier slot vers le slot actuel et décrémenter l'itérateur.

Exemple:

1
2
3
4
5
6
7
8
9
 Callbacks:
 - 1
 - 2
 - 3 
 - 4 < (le callback 4 delete 1)
 - 5
 - 6
 - 7
 - 9

deviendra

1
2
3
4
5
6
7
8
 Callbacks:
 - 4
 - 2
 - 3 <
 - 9
 - 5
 - 6
 - 7

Ça peut paraître simple expliqué comme ça, mais le trouver a été un peu plus long :P

Quoiqu'il en soit, la classe est maintenant implémentée et fonctionnelle, il ne me reste plus qu'à intégrer un Combiner (pour gérer le type de retour) pour avoir toutes les fonctionnalités que je souhaite.

Le code:

  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#define NazaraSignal(SignalName, ...) using SignalName ## Type = NzSignal<__VA_ARGS__>; \
                                       mutable SignalName ## Type SignalName

#define NazaraSlotType(Class, SignalName) Class::SignalName ## Type::ConnectionGuard
#define NazaraSlot(Class, SignalName, SlotName) NazaraSlotType(Class, SignalName) SlotName


template<typename... Args>
class NzSignal
{
    public:
        using Callback = std::function<void(Args...)>;
        class Connection;
        class ConnectionGuard;

        NzSignal();
        NzSignal(const NzSignal&) = delete;
        NzSignal(NzSignal&& signal);
        ~NzSignal() = default;

        void Clear();

        Connection Connect(const Callback& func);
        Connection Connect(Callback&& func);
        template<typename O> Connection Connect(O& object, void (O::*method)(Args...));
        template<typename O> Connection Connect(O* object, void (O::*method)(Args...));
        template<typename O> Connection Connect(const O& object, void (O::*method)(Args...) const);
        template<typename O> Connection Connect(const O* object, void (O::*method)(Args...) const);

        void operator()(Args... args) const;

        NzSignal& operator=(const NzSignal&) = delete;
        NzSignal& operator=(NzSignal&& signal);

    private:
        struct Slot;

        using SlotPtr = std::shared_ptr<Slot>;
        using SlotList = std::vector<SlotPtr>;
        using SlotListIndex = typename SlotList::size_type;

        struct Slot
        {
            Slot(NzSignal* me) :
            signal(me)
            {
            }

            Callback callback;
            NzSignal* signal;
            SlotListIndex index;
        };

        void Disconnect(const SlotPtr& slot);

        SlotList m_slots;
        mutable SlotListIndex m_slotIterator;
};

template<typename... Args>
class NzSignal<Args...>::Connection
{
    using BaseClass = NzSignal<Args...>;
    friend BaseClass;

    public:
        Connection() = default;
        Connection(const Connection& connection) = default;
        Connection(Connection&& connection) = default;
        ~Connection() = default;

        template<typename... ConnectArgs>
        void Connect(BaseClass& signal, ConnectArgs&&... args);
        void Disconnect();

        bool IsConnected() const;

        Connection& operator=(const Connection& connection) = default;
        Connection& operator=(Connection&& connection) = default;

    private:
        Connection(const SlotPtr& slot);

        std::weak_ptr<Slot> m_ptr;
};

template<typename... Args>
class NzSignal<Args...>::ConnectionGuard
{
    using BaseClass = NzSignal<Args...>;
    using Connection = BaseClass::Connection;

    public:
        ConnectionGuard() = default;
        ConnectionGuard(const Connection& connection);
        ConnectionGuard(const ConnectionGuard& connection) = delete;
        ConnectionGuard(Connection&& connection);
        ConnectionGuard(ConnectionGuard&& connection) = default;
        ~ConnectionGuard();

        template<typename... ConnectArgs>
        void Connect(BaseClass& signal, ConnectArgs&&... args);
        void Disconnect();

        Connection& GetConnection();

        bool IsConnected() const;

        ConnectionGuard& operator=(const Connection& connection);
        ConnectionGuard& operator=(const ConnectionGuard& connection) = delete;
        ConnectionGuard& operator=(Connection&& connection);
        ConnectionGuard& operator=(ConnectionGuard&& connection);

    private:
        Connection m_connection;
};
  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
template<typename... Args>
NzSignal<Args...>::NzSignal() :
m_slotIterator(0)
{
}

template<typename... Args>
NzSignal<Args...>::NzSignal(NzSignal&& signal)
{
    operator=(std::move(signal));
}

template<typename... Args>
void NzSignal<Args...>::Clear()
{
    m_slots.clear();
}

template<typename... Args>
typename NzSignal<Args...>::Connection NzSignal<Args...>::Connect(const Callback& func)
{
    return Connect(Callback(func));
}

template<typename... Args>
typename NzSignal<Args...>::Connection NzSignal<Args...>::Connect(Callback&& func)
{
    NazaraAssert(func, "Invalid function");

    bool resetIt = (m_slotIterator >= m_slots.size());

    auto tempPtr = std::make_shared<Slot>(this);
    tempPtr->callback = std::move(func);
    tempPtr->index = m_slots.size();

    m_slots.emplace_back(std::move(tempPtr));
    if (resetIt)
        m_slotIterator = m_slots.size();

    return Connection(m_slots.back());
}

template<typename... Args>
template<typename O>
typename NzSignal<Args...>::Connection NzSignal<Args...>::Connect(O& object, void (O::*method) (Args...))
{
    return Connect([&object, method] (Args&&... args)
    {
        return (object .* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
template<typename O>
typename NzSignal<Args...>::Connection NzSignal<Args...>::Connect(O* object, void (O::*method)(Args...))
{
    return Connect([object, method] (Args&&... args)
    {
        return (object ->* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
template<typename O>
typename NzSignal<Args...>::Connection NzSignal<Args...>::Connect(const O& object, void (O::*method) (Args...) const)
{
    return Connect([&object, method] (Args&&... args)
    {
        return (object .* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
template<typename O>
typename NzSignal<Args...>::Connection NzSignal<Args...>::Connect(const O* object, void (O::*method)(Args...) const)
{
    return Connect([object, method] (Args&&... args)
    {
        return (object ->* method) (std::forward<Args>(args)...);
    });
}

template<typename... Args>
void NzSignal<Args...>::operator()(Args... args) const
{
    for (m_slotIterator = 0; m_slotIterator < m_slots.size(); ++m_slotIterator)
        m_slots[m_slotIterator]->callback(args...);
}

template<typename... Args>
NzSignal<Args...>& NzSignal<Args...>::operator=(NzSignal&& signal)
{
    m_slots = std::move(signal.m_slots);
    m_slotIterator = signal.m_slotIterator;

    // We need to update the signal pointer inside of each slot
    for (SlotPtr& slot : m_slots)
        slot->signal = this;

    return *this;
}

template<typename... Args>
void NzSignal<Args...>::Disconnect(const SlotPtr& slot)
{
    NazaraAssert(slot, "Invalid slot pointer");
    NazaraAssert(slot->index < m_slots.size(), "Invalid slot index");

    // "Swap this slot with the last one and pop" idiom
    // This will preserve slot indexes

    // Can we safely "remove" this slot?
    if (m_slotIterator >= m_slots.size()-1 || slot->index > m_slotIterator)
    {
        // Yes we can
        SlotPtr& newSlot = m_slots[slot->index];
        newSlot = std::move(m_slots.back());
        newSlot->index = slot->index; //< Update the moved slot index before resizing (imagine this is the last one)
    }
    else
    {
        // Nope, let's be tricky
        SlotPtr& current = m_slots[m_slotIterator];
        SlotPtr& newSlot = m_slots[slot->index];

        newSlot = std::move(current);
        newSlot->index = slot->index; //< Update the moved slot index

        current = std::move(m_slots.back());
        current->index = m_slotIterator; //< Update the moved slot index

        --m_slotIterator;
    }

    // Pop the last entry (where we moved our slot)
    m_slots.pop_back();
}


template<typename... Args>
NzSignal<Args...>::Connection::Connection(const SlotPtr& slot) :
m_ptr(slot)
{
}

template<typename... Args>
template<typename... ConnectArgs>
void NzSignal<Args...>::Connection::Connect(BaseClass& signal, ConnectArgs&&... args)
{
    operator=(signal.Connect(std::forward<ConnectArgs>(args)...));
}

template<typename... Args>
void NzSignal<Args...>::Connection::Disconnect()
{
    if (SlotPtr ptr = m_ptr.lock())
        ptr->signal->Disconnect(ptr);
}

template<typename... Args>
bool NzSignal<Args...>::Connection::IsConnected() const
{
    return !m_ptr.expired();
}


template<typename... Args>
NzSignal<Args...>::ConnectionGuard::ConnectionGuard(const Connection& connection) :
m_connection(connection)
{
}

template<typename... Args>
NzSignal<Args...>::ConnectionGuard::ConnectionGuard(Connection&& connection) :
m_connection(std::move(connection))
{
}

template<typename... Args>
NzSignal<Args...>::ConnectionGuard::~ConnectionGuard()
{
    m_connection.Disconnect();
}

template<typename... Args>
template<typename... ConnectArgs>
void NzSignal<Args...>::ConnectionGuard::Connect(BaseClass& signal, ConnectArgs&&... args)
{
    m_connection.Disconnect();
    m_connection.Connect(signal, std::forward<ConnectArgs>(args)...);
}

template<typename... Args>
void NzSignal<Args...>::ConnectionGuard::Disconnect()
{
    m_connection.Disconnect();
}

template<typename... Args>
typename NzSignal<Args...>::Connection& NzSignal<Args...>::ConnectionGuard::GetConnection()
{
    return m_connection;
}

template<typename... Args>
bool NzSignal<Args...>::ConnectionGuard::IsConnected() const
{
    return m_connection.IsConnected();
}

template<typename... Args>
typename NzSignal<Args...>::ConnectionGuard& NzSignal<Args...>::ConnectionGuard::operator=(const Connection& connection)
{
    m_connection.Disconnect();
    m_connection = connection;

    return *this;
}

template<typename... Args>
typename NzSignal<Args...>::ConnectionGuard& NzSignal<Args...>::ConnectionGuard::operator=(Connection&& connection)
{
    m_connection.Disconnect();
    m_connection = std::move(connection);

    return *this;
}

template<typename... Args>
typename NzSignal<Args...>::ConnectionGuard& NzSignal<Args...>::ConnectionGuard::operator=(ConnectionGuard&& connection)
{
    m_connection.Disconnect();
    m_connection = std::move(connection.m_connection);

    return *this;
}

Je suis maintenant à la recherche de suggestions pour voir comment améliorer la classe, ou quelles fonctionnalités pourraient être sympathiques à implémenter :)

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte