2

信号槽机制的简陋实现

 2 years ago
source link: https://dingfen.github.io/cpp/2021/11/15/Qt-signal-slot.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Qt 信号槽机制的简陋实现Permalink

最近科研中碰到了一个需要实现信号触发机制的场景。我第一时间想到了 Qt 的信号槽机制。但可惜服务器上没有 Qt 的相关环境,因此需要自己实现一个简陋的信号触发机制。

问题需求Permalink

假设 A 类的对象 a 与 B 类的对象 b 需要信号连接。即 a 发出某个信号 S 给 b ,b 接收到该信号后,根据自己所处的状态,决定是否响应该信号,响应即调用成员函数 slot(),不响应即让信号进入等待队列。

具体来说,在构造了对象 a 和 b 后,先使用 connect() 函数连接这两个对象。此后,在程序中任何位置中调用了 a->signal() 函数后,就视为对象 a 发出了信号 signal 给 b。此时,若 b 繁忙,那么该信号会暂时等待,直至空闲后 b 再调用成员函数 slot(),若 b 空闲,直接调用成员函数 slot()。与 Qt 机制相同,一个槽函数可以被多个信号相连接,一个信号也可以连接多个槽。

naive 版本Permalink

看到上述需求,我的第一反应是需要建立一个 Connection 类来帮助管理所有的连接。具体来说,在 connect() 函数中需要构造 Connection 类对象 c ,它一方面手握 b 的成员函数,另一方面与 a 的信号挂钩,从而实现信号触发机制。

Conection类中关于成员的探讨

因此,Connection 类的定义有:

class Connection {
    // callback func and itself
    function<void()> m;
    void callback() {
        if (m)
            m();
    }
};

void A::signal() {
    if (c)
        c->callback();
}

对于 connect() 函数,过程就比较简单。构建Connection 类对象后,b 的成员函数传给Connection类中的“函数指针”,另一方面,需要将 a 发出的信号与该函数指针关联起来。

void connect(A* a, B* b, string signal, string slot) {
    Connection *c = new Connection();
    function<void()> f = bind(&B::slot, b);
    c->f = f;
    a->c = &c;
}

关联的方案和 Qt 一样,选择使用字符串匹配的方式。即传入的参数使用宏定义包装,转为字符串。但这里的实现显然还没有用上函数后面的两个参数

#define SIGNAL(x) "1"#x
#define SLOT(x) "2"#x

A *a = new A();
B *b = new B();
connect(a, b, SIGNAL(signal1()), SLOT(slot()))

逐步改进Permalink

响应函数队列Permalink

上述做法很简单,但缺点很多,先是没有使用 connect() 传入的信号和槽,然后是无法支持一个信号对应多个槽函数和多个信号,最后,槽函数的类型被限制为 function<void()> ,这也让人很难受。但总的来说,naive 版本起码说明了,这条路可行。

要想做的更好,需要加上很多东西。首先,对于每个信号,都应该有一个信号槽函数的队列。

class A {
    map<string, vector<Connection *>> connect_map;

    // for all slots connected with signal
    // must callback at once
    void A::signal() {
        for (auto conn : connect_map[SIGNAL(signal())])
            conn->callback();
    }

    void A::setUp(string signal, Connection *c) {
        connect_map[signal].push_back(c);
    }
}

如此,当信号被调用后(发出后),所有与信号相连的槽函数都会被调用。

信号槽函数的抽象Permalink

接下来解决下一个问题:如何使用 connect() 传来的槽函数,而不是硬编码到函数内部。有两个思路:一种是直接将 connect() 的参数类型改为函数指针,因为指针可以直接传入 Connection 类,可方便后续处理,但这会限制槽函数的类型。另一种是仍沿用字符串,但要求内部需要有一个对应表,存放字符串与槽函数的对应关系。参考 Qt 的信号槽机制,我选择使用第二种。

为了让所有类型的函数都可以充当槽函数,又不更改 Connection 类中的变量类型,可以选择抽象包装法

void Slot() {
    switch (func_id) {
    case 0:
        slot1();
        break;
    // case 1:
        // another slot()
        // break;
    default:
        break;
    }
}

直接将上面的 Slot() 函数传给 Connection 类,然后,让对象内的变量通过字符串来确定到底该调用哪一个成员函数。相当于在所有槽函数之上都加了一层抽象。

class B {
    map<string, int> funcMap;
    B() {
        funcMap.insert(pair<string, int>(SLOT(slot1()), 0));
    }
    void findFuncId(string slot) {
        if (funcMap.find(slot) != funcMap.end())
            func_id = funcMap[slot];
        else 
            func_id = -1;
    }
}

findFuncId() 将字符串转为内部的函数编号,进而改变了 Slot() 函数要调用的槽函数,但需要我们提取把槽函数和编码都放到表中。为了统一,在信号发送端,我们也希望用整数取代字符串,来唯一编号信号函数。

class A {
    map<string, int> sgMap;
    A() {
        sgMap.insert(pair<string, int>(SIGNAL(signal1()), 0));
    }
    int findSetId(string slot) {
        if (sgMap.find(slot) != sgMap.end())
            signal_id = sgMap[slot];
        else 
            signal_id = -1;
        return signal_id;
    }
}

从上面的改进中,可以看出不论槽函数是什么样,在 Connection 类中的函数指针永远指向 Slot()。对于 Connection 类而言,只需要记住哪个信号发生后(即信号的编码),需要触发哪些对象的哪些槽函数(即对应对象的槽函数编号)就行了。

Object 类Permalink

不难看到,A 类和 B 类在有些地方有相似性,可将其抽象出来成为一个基类 Object。另一方面,Object 类也可以替代 Connection 类的功能。所以,需要在这个地方做一个非常大的改动。

// Old Connection
class Connection {
    // callback func and itself
    function<void()> m;
    void callback() {
        if (m)
            m();
    }
};

// Now Connection
struct Connection {
    int id;
    Object* receiver;
};

首先,将 Connection 类做了改变,因为只需要槽函数的编号和类的抽象包装函数即可。

A 类和 B 类中,都有对信号/槽函数的编码表,那么编码表可以放在 Object 类中,取名为 funcMap,提供函数名字符串返回其编码。

此外,将之前 A 类中的 map<string, vector<Connection *>> connect_map 信号与对应槽函数的对应表关系也放入 Object 类中。把 B 类的抽象槽函数 Slot() 也放进 Object 类。如此,只要继承了 Object 类,再稍加修改(后续介绍),就可以使用我们自己实现的信号槽机制,与 Qt 的非常类似。

class Object {
public:
    map<int, vector<Connection>> connect_map;
    map<string, int> funcMap;
    function<void(int)> Slot_;
}

最后,在 Object 类中添加之前的函数实现,包括 connect() 函数。

class Object {
    void activate(int id) {
        for(auto c : connect_map[id])
            c.receiver->Slot_(c.id);
    }

    int findId(const string& s) {
        int func_id = -1;
        if (funcMap.find(s) != funcMap.end())
            func_id = funcMap[s];
        return func_id;
    }

    static void connect(Object *sender, Object *receiver,
            const char* signal, const char* slot) {
        int sid = sender->findId(signal);
        int rid = receiver->findId(slot);
        Connection c(rid, recevier);
        sender->connect_map[sid].push_back(c);
    }
};

A 类和 B 类都直接继承 Object 类。但这样做还不够,需要在各自的构造函数中把自己的信号/槽函数加入 funcMap 中,槽函数也需要被动态绑定:

class A : public Object {
public:
    A() {
        funcMap.insert(pair<string, int>(SIGNAL(signal1()), 0));
    }
};

class B : public Object {
public:
    B() {
        Slot_ = bind(&B::Slot, this, placeholders::_1);
        funcMap.insert(pair<string, int>(SLOT(slot1()), 0));
    }
};

这样改动的好处是,要添加一个槽函数,我们无需更改 Object 类的部分,只需要更改 B 类的构造函数和 Slot()。新添加一个信号也是如此。

带参数的信号槽Permalink

最后的改动,以支持某一些简单的参数可以在信号中传递。首先,在 Object 类中的槽函数,添加一个 void** 的变量,用于传参。随之改动的,就是 activate()void ** 相当于一个指向所有参数的指针数组。

class Object {
    function<void(int, void**)> Slot_;

    void activate(int id, void **arg) {
        for(auto c : connect_map[id]) {
            c.receiver->Slot_(c.id, arg);
        }
    }
}

在 A 类和 B 类中,为了与 Object 类一致,做如下修改:

class B {
    B() {
        Slot_ = bind(&B::Slot, this, placeholders::_1, placeholders::_2);
        // ....
    }
    
    void Slot(int id, void **args) {
        switch (id) {
        case 0:
            slot1(*reinterpret_cast<int*>(args[0]));
            break;
        }
    }
    void slot1(int a) {
        cout << "Slot : " << a << endl;
    }
}

// In Class A
    void signal1(int a) {
        void *arg[] = {reinterpret_cast<void*>(&a)};
        activate(0, arg);
    }

信号函数中的多个参数,需要为它们一一构建好 void* 指针。在 Slot() 中,再将它们一一拆解开。

小结Permalink

通过自己不断思考尝试,实现了 Qt 信号槽机制的简陋版。在失去了 MOC 机制后,编写自己的槽机制会有点丑陋,并且有很多地方需要改进,先勉强用上吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK