Общая система сообщения с помощью шаблонов с переменным количеством аргументов


в свободное им время на создание игрового движка хобби, как решил написать некоторые из моих систем на мой собственный. Как проходит время, я решил начать публикацию ее в качестве независимой библиотеки для использования (так что любой может использовать его), первая-моя системное сообщение. Главная задача была проста : создать гибкую систему, которая позволила мне создавать различные типы сообщений даже во время выполнения. Для этого я использовал современный c++, таких как шаблоны с переменным числом аргументов, лямбда-выражений, интеллектуальных указателей (с целью снижения утечек памяти). Я использовал библиотеку RTTI (ИД типа) из-за механизма для определения заданных параметров при прослушивании/привязки к сообщению.

Некоторые простые идеи :

  1. Создавать сообщения, сохранять их хэш(т. е. вариативных параметров) - с помощью метода createmessage функции
  2. Привязать функции к сообщениям (при привязке функции, проверка функции параметров, если он соответствует критериям, которые были даны созданного сообщения) - через bindFunction
  3. Широковещательные сообщения

Для простоты я определил его как один-заголовок библиотеки из-за легкости компиляция и переносимость. Он написан на ООП, сообщения создаются только через itnerface из главного класса, MessageManager(статический класс на данный момент) и слушая,+вещание осуществляется через объекты, называемые MessageHandlers.

    #pragma once

#include <vector>
#include <unordered_map>
#include <typeinfo>
#include <functional>
#include <utility>
#include <algorithm>
#include <memory>

namespace pin
{
    const std::string PIN_BLANK = "Blank";

    //Variadic function wrapper
    template<class... Args>
    class variadicFunction
    {
        private:
            //Functor
            std::function<void(Args...)> _function;

        public:
            variadicFunction() {};

            template<class T>
            void bindFunction(void(T::*function)(Args... args), T* ownerPtr)
            {
                //Saved via lambda to avoid generic placeholders with std::bind
                _function = [function, ownerPtr](Args... args)
                {
                    (ownerPtr->*function)(args...);
                };
            }

            void exec(Args&&... args)
            {
                if (_function)
                    _function(std::forward<Args>(args)...);
            }
    };

    //Base class for parameter pack so its able to combine it with polymorphism
    class base_param_pack{};

    //Variadic parameter pack
    template<class... Args>
    class param_pack{};

    class BaseMessage
    {
    protected:
        //Text of the message
        std::string _messageText;

        //Hash generated to check the type of the passed parameter pack
        std::size_t _parameterHash;

    protected:

        //Hashing function
        template <class... Args>
        void createHash()
        {
            //Create temporary parameter pack class from our passed arguments
            param_pack<Args...> tmpPack;

            //Save the ID of the pack using the RTTI
            _parameterHash = typeid(tmpPack).hash_code();

        }

    public:
        //Default constructor
        BaseMessage() :
            _messageText(PIN_BLANK),
            _parameterHash(0)
        {}

        //Constructing with the message name
        BaseMessage(const std::string& messageName) :
            _messageText(messageName),
            _parameterHash(0)
        {}

        virtual ~BaseMessage() {};

        virtual void clearListeners() = 0;

        //Checking the parameter hashes
        bool haveSameParameterHash(const std::size_t& paramHash)
        {
            return (_parameterHash == paramHash);
        }

        //Setting the message text
        void setMessageText(const std::string& text) { _messageText = text; }
        const std::string& getMessageText() { return _messageText; }
    };

    template <class... Args>
    class Message :public BaseMessage
    {
        typedef std::pair<bool*, variadicFunction<Args...>> listener;
    private:

        //Vector of listeners represented as variadic functions
        std::vector<listener> _listeners;

    protected:

        //Function checking if the listener exists or not
        bool isValidListener(const listener& listener)
        {
            return (listener.first);
        }

        void clearListeners() override
        {
            //Erase-remove idiom
            _listeners.erase(std::remove_if(
                                            _listeners.begin(),
                                            _listeners.end(),
                                            [](const listener& listener) 
                                            {
                                                return !*listener.first;
                                            }
                                            ),
                            _listeners.end());
        }

    public:

        Message():
        {
            createHash();
        }

        Message(const std::string& messageName) :
            BaseMessage(messageName)
        {
            createHash<Args...>();
        }

        //Broadcasting the message - no check if the parameter pack sets the hash
        void broadcast(Args&&... args)
        {
            for (auto& listener : _listeners)
            {
                listener.second.exec(std::forward<Args>(args)...);
            }
        }

        //Binding the function listener - no check if the parameter pack sets the hash
        template<class T, class... Args2>
        void bindFunction(bool* handler, void(T::*function)(Args2... args), T* ownerPtr)
        {
            _listeners.emplace_back();

            _listeners.back().first = handler;

            _listeners.back().second.bindFunction(function, ownerPtr);
        }
    };

    typedef std::unordered_map<std::string, std::unique_ptr<BaseMessage>> MessageMap;

    //Class that creates messages
    class MessageManager
    {
        friend class MessageHandler;

        private:
            static MessageMap _messages;

            /*
            *   Replace this with your own behaviour for giving bad hash 
            */
            static void incorrectHash()
            {
                std::cout << "pin::MessageSystem : The hash of given arguments do not fit" << std::endl;
            }

            /*
            *   Replace this with your own behaviour for giving bad message name
            */
            static void incorrectMessage()
            {
                std::cout << "pin::MessageSystem : Message not found" << std::endl;
            }

        protected:

            template<class T, class... Args>
            static BaseMessage* listenToMessage(bool* handler, const std::string& messageName, void(T::*function)(Args... args), T* ownerPtr)
            {
                //Check if the message with messageName exists
                auto it = _messages.find(messageName);

                if (it != _messages.end())
                {
                    //Create temporary hash
                    std::size_t tmpHash = typeid(param_pack<Args...>).hash_code();

                    //Check the created hash with the hash of the message
                    if (it->second->haveSameParameterHash(typeid(param_pack<Args...>).hash_code()))
                    {
                        //Bind the function
                        static_cast<Message<Args...>*>(it->second.get())->bindFunction(handler, function, ownerPtr);

                        return it->second.get();
                    }
                    else
                    {
                        /*
                        * User tried to pass a different parameter pack that was passed to message when you created it. Pass your own behavior here
                        */
                        incorrectHash();
                    }
                }
                else
                {
                    /*
                    * User tried to find a message that does not exists, put your own behaviour here
                    */
                    incorrectMessage();

                }

                return nullptr;
        }

        template<class... Args>
        static void broadcastMessage(const std::string& messageName, Args... args)
        {
            //Check if the message with messageName exists
            auto it = _messages.find(messageName);

            if (it != _messages.end())
            {
                //Generated hash for current parameter pack
                std::size_t tmpHash = typeid(param_pack<Args...>).hash_code();

                //Check if its the right hash
                if (it->second->haveSameParameterHash(typeid(param_pack<Args...>).hash_code()))
                {
                    //If its right broadcast the message
                    static_cast<Message<Args...>*>(it->second.get())->broadcast(std::forward<Args>(args)...);
                }
                else
                {
                    incorrectHash();
                }
            }
            else
            {
                incorrectMessage();
            }
        }

        public:

            template<class... Args>
            static void createMessage(const std::string& messageName)
            {
                _messages[messageName] = std::make_unique<Message<Args...>>(messageName);
            }
    };

    MessageMap MessageManager::_messages;

    //Class that can listen to messages or broadcast them
    class MessageHandler
    {
            private:
                bool _active;

                std::vector<BaseMessage*> _listenedMessages;
            public:

                MessageHandler():
                    _active(true){}

                ~MessageHandler()
                {
                    if (_active)
                        deleteHandler();
                }

                template<class T, class... Args>
                void listenToMessage(const std::string& messageName, void(T::*function)(Args... args), T* ownerPtr)
                {
                    //Vector of messages being listened to by this handler
                    _listenedMessages.emplace_back(MessageManager::listenToMessage(&_active, messageName, function, ownerPtr));
                }

                template<class... Args>
                void broadcastMessage(const std::string& messageName, Args... args)
                {
                    //Broadcast message
                    MessageManager::broadcastMessage(messageName, args...);
                }

                void deleteHandler()
                {
                    _active = false;

                    //All messages must clear their listeners - delete those who are inactive
                    for (auto& message : _listenedMessages)
                        message->clearListeners();
                }
    };
}

В контактный префикс является префиксом, который я использую для моих творений. Код полностью размещены на github, а также с какой-нибудь учебник для использования : GitHub ссылке

Как я просто новичок в программировании, я бы хотел услышать обратную связь от вас, ребята. Особенно плохо то, что должно быть улучшено. Спасибо



495
4
задан 27 марта 2018 в 06:03 Источник Поделиться
Комментарии
1 ответ

Четко определить вариант использования

Когда я думаю о "системы сообщений", либо передачи сообщений между различными компьютерами, процессов или потоков. Однако, похоже, что ваша система сообщений работает только в пределах одного потока кода. Кроме того, это действительно не хранить и пересылать сообщения, он просто регистрирует и вызывает функции обратного вызова. Это нормально, и может быть именно то, что вы хотите, но сделать это абсолютно ясным для потенциальных пользователей вашей системы сообщение.

Ручки по сравнению с именами

Ваше сообщение системы определяет типы сообщений по имени. Тем не менее, вы должны программно зарегистрировать типы сообщений. Вместо того, чтобы использовать неупорядоченный карту ассоциировать типы сообщений с именами, почему нет MessageManager::createMessage<>() возвращает ссылку на Message объект? Таким образом, вы можете передать эту ссылку listenToMessage() и broadcastMessage()вместо имени, и не делать карту поиска каждый раз.

Избежать глобального состояния

Ваш MessageMap является глобальной переменной. Что произойдет, если у вас есть программа, которая связывает с несколькими библиотеками, каждая из которых использует свою систему сообщений? Они будут использовать тот же MessageMap. Это может привести к конфликтам.

Вы уже сделали так, что вы должны объявить MessageManager переменные, прежде чем вы можете послушать и отправить MessageС. Почему бы не сделать MessageMap нестатическую переменную-член MessageManager?

Проверить для нескольких регистраций одного и того же имени

Ваш код не проверить, если программа пытается вызвать MessageManager::createMessage<>() дважды с тем же именем. Это будет просто перезаписать запись в MessageMap со вторым Messageэкземпляр.
Или не используйте имена или проверить, что вы не добавить то же имя дважды (или наоборот, позволит двойных регистраций, но только до тех пор, как они все регистрируют один и тот же тип сообщения).

hash_code() не гарантируется, чтобы быть уникальным

Вы используете hash_code() чтобы проверить, является ли тип параметра пакета такая же, как Message. Однако, в то время как hash_code() гарантирует, что идентичные типы получить тот же хэш-значение, нет никакой гарантии, что два разных вида получить другой хэш-значение.

Если вы не используете сообщения названий, а есть ссылка на настоящее Message объект, а затем вы могли двигаться такие функции, как listenToMessage(), broadcastMessage() к Message класс. Тогда компилятор будет точно знать тип пакет параметров, без необходимости полагаться на проверки времени выполнения.

Использовать std::function<> для обратных вызовов

Вместо того чтобы заставлять обратных вызовов, чтобы быть указателями на функции-члены, и требующие
пользователь может задавать как функцию-член и объект по отдельности, использовать std::function<> представляет функцию обратного вызова. Преимущество в том, что он будет принимать любой тип функции обратного вызова, то ли им функции, лямбда-выражения, или функция-член (используя std::bind). Сделать это вот так:

template<class... Args>
void listenToMessage(const std::string& messageName, std::function<void(Args...)> callback)
{
...
}

Чтобы пользователи могли задать функцию-член класса в качестве обратного вызова, он будет называться так:

Foo foo;
tut.listenToMessage("foo", std::bind(&Foo::test, foo));

Но это делает его гораздо легче работать с нечленами функции или лямбда-выражения:

void non_member(int i) { std::cout << i; }
MessageManager::createMessage<int>("foo");
tut.listenToMessage("foo", non_member);
tut.listenToMessage("foo", [](int i){ std::cout << i; })

3
ответ дан 27 марта 2018 в 07:03 Источник Поделиться