Как реализовать «соединения» с помощью функторов C ++ - PullRequest
1 голос
/ 13 июля 2020

Я работаю над кодом для реализации облегченной версии чего-то похожего на механизм сигналов / слотов Qt (т.е. прославленный шаблон наблюдателя), где я могу соединять «сигналы» с «слотами». «Соединение» может быть выполнено только в том случае, если две подписи идентичны. Каждый раз, когда испускается «сигнал», все вызовы любых подключенных «слотов» будут помещены в очередь для выполнения в соответствующих потоках в более позднее время.

Я провел большое количество исследований на это topi c и понимаю, что то, что я хочу, может быть достигнуто с помощью некоторой комбинации функторов и шаблонов. Однако я не могу понять, как заставить все работать так, как мне хотелось бы. Кроме того, это используется во встроенном процессоре, поэтому я не хочу использовать std :: function, который, как я читал, связан с большим объемом накладных расходов.

Пока что у меня есть написал успешную подпись для функции "подключения" следующим образом:

template<typename OBJECT, typename FUNC>
static void connect(OBJECT *sender, FUNC signal, OBJECT *receiver, FUNC slot) {

}
//...
Test1 t;
Test1::connect(&t, &Test1::signal1, &t, &Test1::slot1);

Теперь мне нужен способ сохранить вызов функции, связанный с объектом / слотом, который будет сохранен и вызван сигналом, когда он испускается. Я понимаю, что это нужно делать с помощью Functor. Однако я не могу понять, как написать функтор, который является c объектом, но требует специальной c подписи. Я ищу что-то вроде:

GenericFunctor<int, int> slotsConnectedToSignal1[4];
GenericFunctor<int, char, int> slotsConnectedToSignal2[4];

Таким образом, сигнал (который имеет ту же сигнатуру, что и массив, содержащий его подключенные слоты) может l oop через массив и вызывать все функтор.

Есть ли способ достичь того, что я пытаюсь выполнить sh и на правильном ли я пути?

Спасибо!

РЕДАКТИРОВАТЬ

Я приближаюсь к тому, что хочу, используя следующие определения для connect ().

template <typename ObjSender, typename Ret, typename ObjReceiver>
static void connect(ObjSender *sender, Ret(ObjSender::*signal)(), ObjReceiver *receiver, Ret(ObjReceiver::*slot)()) {
    std::function<Ret()> fSender = std::bind(signal, sender);
    std::function<Ret()> fReceiver = std::bind(slot, receiver);
}

template <typename ObjSender, typename Ret, typename ARG0, typename ObjReceiver>
static void connect(ObjSender *sender, Ret(ObjSender::*signal)(ARG0), ObjReceiver *receiver, Ret(ObjReceiver::*slot)(ARG0)) {
    std::function<Ret(ARG0)> fSender = std::bind(signal, sender, std::placeholders::_1);
    std::function<Ret(ARG0)> fReceiver = std::bind(slot, receiver, std::placeholders::_1);
}

Теперь мой следующий вопрос - как сохранить и вызвать эти std :: функциональные объекты в их правильных сигналах. Например, когда пользователь вызывает signal1 (1, 2), эта функция должна иметь возможность искать все связанные с ней «связанные» объекты std :: function и вызывать каждый из них по очереди с аргументами.

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

EDIT 2

Основываясь на некоторых полученных мной отзывах, это моя последняя попытка достичь желаемых результатов.

template<typename ... ARGS>
class Signal {
public:
    void operator()(ARGS... args) {
        _connection(args...);
    }

    void connect(std::function<void(ARGS...)> slot) {
        _connection = slot;
    }

private:
    std::function<void(ARGS...)> _connection;
};

class Test2 {
public:
    Signal<int, int> signal1;
    Signal<int, int> signal2;

    void slot1(int a, int b) {
        signal1(a, b);
    }
    void slot2(int c, int d) {
        int i = c + d;
        (void)i;
    }
};
int main(void) {
    Test2 t2;

    t2.signal1.connect(t2.signal2);
    t2.signal2.connect(std::bind(&Test2::slot2, &t2, std::placeholders::_1, std::placeholders::_2));
    t2.slot1(1, 2);
}

Однако в этом случае у меня все еще есть проблема: когда я хочу подключиться к функции «слот» (вместо другого сигнала), мне нужно использовать std :: bind с нужным количеством заполнителей. Я знаю, что должен быть способ сделать это, но я недостаточно знаком с std :: function и лямбда-выражениями.

Ответы [ 2 ]

0 голосов
/ 30 июля 2020

Я думаю, что наконец-то придумал метод, который работает для меня, основываясь на всех полученных мной комментариях. Вот упрощенная версия для всех, кому интересно:

template<typename ... ARGS>
class Signal {
    #define MAX_CONNECTIONS 4

public:
    #define CONNECT_FAILED (ConnectionHandle)(-1);

    Signal() : _connections{nullptr} {};

    /**
     * @brief Implementation of the "function" operator
     *
     * @param args Arguments passed to all connected slots (or signals)
     */
    void operator()(ARGS... args) {
        //Loop through the connections array
        for (int i = 0; i < MAX_CONNECTIONS; i++) {
            if (_connections[i]) {
                /*
                 * Call the connected function
                 * This will either
                 * a) Call a lambda which will invoke a slot
                 * b) Call operator() on another signal (i.e. recursive signals)
                 */
                _connections[i](args...);
            }
        }
    }

    /**
     * @brief Make a connection to a slot
     *
     * @param t A pointer to the object instance
     * @param fn A member function pointer to the slot
     * @return A handle (ID) used to disconnect the connection if desired
     *
     * @note This function assumes that T is a subclass of ActiveObject (i.e. has the invoke() method)
     */
    template<class T>
    inline ConnectionHandle connect(T* t, void(T::* fn)(ARGS...)) {
        //This lambda will use ActiveObject::invoke to queue the connected slot for later execution
        const auto lambda = [=](ARGS... args) { T::invoke(t, fn, args...); };
        return connect(lambda);
    }

    /**
     * @brief Make a connection to another signal
     *
     * @param t A pointer to the object instance
     * @param s The signal
     * @return A handle (ID) used to disconnect the connection if desired
     */
    template<class T>
    inline ConnectionHandle connect(T *t, Signal<ARGS...> T::*s) {
        return connect(t->*s);
    }


    /**
     * @brief Make a generic connection to a slot which takes an Event smart pointer as its argument
     *
     * @param t The object to connect to
     * @param fn The member function of t to connect to
     * @param eventIndex The user-defined index to assign to the event
     * @return A handle (ID) used to disconnect the connection if desired
     *
     * @note This version of connect is useful to connect any signal to the same slot function which may dispatch the eent directly into its state machine (if derived from StateMachine)
     */
    template<class T>
    inline ConnectionHandle connect(T *t, void(T::* fn)(std::shared_ptr<Event> e), int eventIndex) {
        const auto lambda = [=](ARGS... args){
            std::shared_ptr<Event> ptr = std::make_shared<Event>(eventIndex);

            T::invoke(t, fn, ptr);
        };
        return connect(lambda);
    }

    /**
     * @brief Make a connection to an abstract function
     *
     * @param slot The function to connect to
     * @return A handle (ID) used to disconnect the connection if desired
     */
    ConnectionHandle connect(std::function<void(ARGS...)> slot) {
        ConnectionHandle i;

        //2nd: Make the connection in an empty slot
        for (i = 0; i < MAX_CONNECTIONS; i++) {
            if (!_connections[i]) {
                //Make the connection
                _connections[i] = slot;

                return i;
            }
        }

        return CONNECT_FAILED;
    }

    /**
     * @brief Remove the given connection by its handle (i.e. ID)
     * @param h The handle previously returned by a call to connect()
     */
    void disconnect(ConnectionHandle h) {
        if ((h < 0) || (h >= MAX_CONNECTIONS)) return;

        _connections[h] = nullptr;
    }

private:
    std::function<void(ARGS...)> _connections[MAX_CONNECTIONS];
};

Где любой объект, который хочет использовать сигналы и слоты, должен подклассифицировать следующее:

class ActiveObject {
public:
    #define DECLARE_SIGNAL(name,...) Signal<__VA_ARGS__> name
    #define EMIT
    #define DECLARE_SLOT(name, ...) void name(__VA_ARGS__)
    #define DEFINE_SLOT(className, name, ...) void className::name(__VA_ARGS__)

    ActiveObject(ActiveObjectThread *parentThread);
    virtual ~ActiveObject();

    /**
     * @brief Called by the parent thread during initialization once the thread has started and is running
     * @note This function may be overridden by sub-classes to provide initialization code
     */
    virtual void initialize() {};

    /**
     * @brief Return the parent thread of this active object
     */
    inline ActiveObjectThread *thread() const { return _parentThread; }

    /**
     * @brief Queue a slot to be called later by the parent thread
     *
     * @param t A pointer to the active object
     * @param fn A member function pointer within the active object to execute
     * @param args The arguments to pass to the slot function when called
     * @note Invoke should ALWAYS be used when calling a slot function to ensure that it is executed within the same thread as the active object (i.e. the parent thread)
     */
    template<class T, typename ... ARGS>
    inline static void invoke(T *t, void(T::* fn)(ARGS...), ARGS... args) {
        std::function<void()> *f = new std::function<void()>([=]() { (t->*fn)( args... ); });

        //Queue in the parent thread
        t->_parentThread->queueInvokable(f);
    }


    inline static void invoke(ActiveObject *ao, std::function<void()> f) {
        std::function<void()> *newF = new std::function<void()>(f);

        ao->_parentThread->queueInvokable(newF);
    }
private:
    ActiveObjectThread *_parentThread;
};

В моем приложении , Я не вызываю ни один из слотов напрямую, а ставлю их в очередь для последующего выполнения потоком.

Вот пример того, как использовать эти классы:

class MyActiveObject : public ActiveObjecte { 
public:
    MyActiveObject() :
        //Just create a thread as part of the object's constructor
        ActiveObject(new ActiveObjectThread("MyActiveObject", 512, 1))
    {
        thread()->Start();
    }
    ~MyActiveObject() {
        //Make sure to delete the thread we created
        delete thread();
    }

    DECLARE_SIGNAL(signal1, int, int);

    DECLARE_SLOT(slot1) {
        GenericEvent *e = new GenericEvent(EVENT_1);

        e->args()[0] = 100;

        //Dispatch the event into the state machine
        dispatch(e);

        EMIT signal1(5, 6);
    }

    DECLARE_SLOT(slot2, int a, int b) {
        GenericEvent *e = new GenericEvent(EVENT_2);

        e->args()[0] = a;
        e->args()[1] = b;

        //Dispatch the event into the state machine
        dispatch(e);
    }

    DECLARE_SLOT(slotEventHander, std::shared_ptr<Event> e) {
        dispatch(e.get());
    }
};

MyActiveObject myActiveObject;
myActiveObject.signal1.connect(&myActiveObject, &MyActiveObject::slot2);
myActiveObject.signal1.connect(&myActiveObject, &MyActiveObject::slotEventHander, MyActiveObject::EVENT_2);
ActiveObject::invoke(&myActiveObject, &MyActiveObject::slot1);

I вынули код, реализующий конечный автомат, потому что он не имеет отношения к этому топу c.

Надеюсь, это кому-то поможет!

0 голосов
/ 16 июля 2020

То, что вы делаете в своем main() в «EDIT 2», немного запутано, но основная проблема c заключается в том, что ваш первый вызов connect() создает копию объекта signal1, который является не то же самое, что ссылка на переменную-член t2. Один из способов решить эту проблему - захватить его с помощью лямбда:

#include <iostream>
#include <functional>
#include <vector>

template<typename ... ARGS>
class Signal {
public:
    void operator()(ARGS... args) const {
        for( const auto& slot : _connection ) {
            slot(args...);
        }
    }

    // Return the index as a handle to unregister later
    auto connect(std::function<void(ARGS...)> slot) {
        _connection.emplace_back( std::move( slot ) );
        return _connection.size() - 1;
    }

    // ... unregister function, etc.

private:
    std::vector<std::function<void(ARGS...)>> _connection;
};

class Test2 {
public:
    Signal<int, int> signal1;
    Signal<int, int> signal2;

    void slot1(int a, int b) {
        std::cout << "slot1 " << a << ' ' << b << '\n';
        signal1(a,b);
    }
    void slot2(int c, int d) {
        std::cout << "slot2 " << c << ' ' << d << '\n';
    }
};

int main() {
    Test2 t2;

    //t2.signal1.connect( t2.signal2 ); // Makes a copy of t2.signal2
    t2.signal1.connect( [&]( auto x, auto y ) { t2.signal2(x,y); } ); // Keeps a reference to t2.signal2
    t2.signal2.connect( [&]( auto x, auto y ) { t2.slot2( x, y ); } );
    t2.signal1(1, 2);
}

Посмотреть в прямом эфире Wandbox , где он вызывает только slot2(), как ожидалось:

slot2 1 2

FWIW, вот тот же код (но короче!) С использованием Boost.Signals2:

#include <iostream>
#include <boost/signals2.hpp>

class Test2 {
public:
    boost::signals2::signal<void(int, int)> signal1;
    boost::signals2::signal<void(int, int)> signal2;

    void slot1(int a, int b) {
        std::cout << "slot1 " << a << ' ' << b << '\n';
        signal1(a,b);
    }
    void slot2(int c, int d) {
        std::cout << "slot2 " << c << ' ' << d << '\n';
    }
};

int main() {
    Test2 t2;

    //t2.signal1.connect( t2.signal2 );
    t2.signal1.connect( [&]( auto x, auto y ) { t2.signal2(x,y); } );
    t2.signal2.connect( [&]( auto x, auto y ) { t2.slot2( x, y ); } );
    t2.signal1(1, 2);
}

Единственные отличия - #include s, объявления signal1 и signal2, а также удаление класса home-spun Signal. Посмотрите вживую на Wandbox .

Обратите внимание, что когда Qt делает это, он полагается на макросы и MO C для генерации дополнительного связующего кода, чтобы сделать это все работает.

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

#include <iostream>
#include <functional>
#include <utility>
#include <vector>

template<typename ... ARGS>
class Signal {
public:
    void operator()(ARGS... args) const {
        for( const auto& slot : _connection ) {
            slot(args...);
        }
    }

    template<class T>
    auto connect(T& t, void(T::* fn)(ARGS...)) {
        const auto lambda = [&t, fn](ARGS... args) { 
            (t.*fn)( std::forward<ARGS>( args )... ); 
        };
        return connect( lambda );
    }

    auto connect(std::function<void(ARGS...)> slot) {
        _connection.emplace_back( std::move( slot ) );
        return _connection.size() - 1;
    }

private:
    std::vector<std::function<void(ARGS...)>> _connection;
};

class Sender {
public:
    Signal<int, int> signal1;
};

class Receiver {
public:
    void slot1(int a, int b) {
        std::cout << "slot1 " << a << ' ' << b << '\n';
    }
    void slot2(int a, int b) {
        std::cout << "slot2 " << a << ' ' << b << '\n';
    }
};

// Stand-alone slot
void slot3(int a, int b) {
    std::cout << "slot3 " << a << ' ' << b << '\n';
}

int main() {
    auto sender = Sender{};
    auto recv   = Receiver{};

    // Register three different slots
    sender.signal1.connect( [&]( auto x, auto y ) { recv.slot1( x, y ); } );
    sender.signal1.connect( recv, &Receiver::slot2 );
    sender.signal1.connect( &slot3 );

    // Fire the signal
    sender.signal1(1, 2);
}

Я немного отрефакторировал и, надеюсь, упростил поиск . Смотрите вживую на Wandbox , где он выводит ожидаемые:

slot1 1 2
slot2 1 2
slot3 1 2
...