如何使用 C++ 仿函数实现 "Connections"

How to Implement "Connections" Using C++ Functors

我正在编写代码来实现类似于 Qt 的 signals/slots 机制(即美化的观察者模式)的轻量级版本,我可以在其中将“信号”连接到“插槽”。仅当两个签名相同时才能建立“连接”。任何时候发出“信号”,所有对任何附加“插槽”的调用都将排队等待稍后在其适当的线程中执行。

我一直在对这个主题进行大量研究,并且明白我想要的可以通过 Functors 和模板的某种组合来实现。但是,我无法弄清楚如何让一切都按照我想要的方式工作。此外,这是在嵌入式处理器上使用的,所以我不想使用 std::function,正如我所读,它有大量与之相关的开销。

至此,我已经为一个“connect”函数写了一个成功的签名如下:

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);

现在,我需要一些方法来存储与 object/slot 关联的函数调用,以便在信号发出时存储和调用它。我知道这应该用 Functor 来完成。但是,我无法弄清楚如何编写一个与对象无关但需要特定签名的仿函数。我正在寻找类似以下内容的内容:

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

这样,信号(与包含其连接槽的数组具有相同的签名)可以循环遍历数组并调用所有的函子。

有什么方法可以实现我想要完成的目标吗?我走在正确的轨道上吗?

谢谢!

编辑

使用以下 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::function 对象。例如,当用户调用 signal1(1, 2) 时,此函数应该能够查找与其关联的所有“已连接”std::function 对象,并使用参数依次调用每个对象。

此外,我需要指出的是,此代码是针对嵌入式系统的,这就是为什么我试图从头开始开发此代码以最大程度地减少外部库的开销。

编辑 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 和 lambdas 的工作还不够熟悉。

您在“编辑 2”中的 main() 中所做的事情有点令人费解,但基本问题是您的第一个 connect() 调用正在复制 signal1 对象,这与对 t2 的成员变量的引用不同。解决这个问题的一种方法是用 lambda 捕获它:

#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);
}

唯一的区别是 #includesignal1signal2 的声明,以及删除了自制的 Signal class .在 Wandbox.

上观看直播

请注意,当 Qt 执行此操作时,它依赖于宏和 MOC 来生成一些额外的粘合代码以使其全部正常工作。


更新:回复你的评论,是的。为了支持该语法,您可以添加一个为用户执行绑定工作的重载。您可能会因必须为不同的 const-nesses 提供多个重载而遇到一些麻烦,但这应该给您一个想法:

#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

根据我收到的所有评论,我想我终于想出了一个适合我的方法。以下是任何感兴趣的人的简化版本:

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);

我已经把实现状态机的代码拿出来了,因为那与本主题无关。

希望对大家有所帮助!