1+1=10

记记笔记,放松一下...

用ISO C++实现自己的信号槽(Qt另类学习)

有网友抱怨:

哪个大牛能帮帮我,讲解一下信号槽机制的底层实现? 不要那种源码的解析,只要清楚的讲讲是怎么发送信号,怎么去选择相应的槽,再做出反应。也就是类似于一个信号槽的相应流程。。。求解啊!!! 看了源码,真的是一头雾水。。。撞墙的心都有了~~~~

本文使用 ISO C++ 一步一步实现了一个极度简化的信号与槽的系统 (整个程序4个文件共121行代码) 。希望能有助于刚进入Qt世界的C++用户理解Qt最核心的信号槽与元对象系统是如何工作的。

另:你可能会对 从 C++ 到 Qt 一文感兴趣

注:Qt5 已经引入一种全新的信号与槽的语法:信号可以和普通的函数、类的普通成员函数、lambda函数连接(而不再局限于信号函数和槽函数)。

Qt信号与槽

GUI程序中,当我们我们点击一个按钮时,我们会期待我们自定义的某个函数被调用。对此,较老的工具集(toolkits)都是通过回调函数(callback)来实现的,Qt的神奇之处就在于,它使用信号(signal)与槽(slot)的技术来取代了回调。

在继续之前,我们先看一眼最最常用的 connnect 函数:

1
connect(btn, "2clicked()", this, "1onBtnClicked()")

可能你会觉得稍有点眼生,因为为了清楚起见,我没有直接使用大家很熟悉的SIGNAL和SLOT两个宏,宏定义如下:

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

程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?C++的经验可以告诉我们:

  • 类中应该保存有信号和槽的字符串信息
  • 字符串和信号槽函数要关联

而这,就是通过神奇的元对象系统所实现的(Qt的元对象系统预处理器叫做moc,对文件预处理之后生成一个moc_xxx.cpp文件,然后和其他文件一块编译即可)。

接下来,我们不妨尝试用纯C++来实现自己的元对象系统(我们需要有一个自己的预处理器,本文中用双手来代替了,预处理生成的文件是db_xxx.cpp)。

继续之前,我们可以先看一下我们最终的类定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Object    
{    
    DB_OBJECT  
public:    
    Object();    
    virtual ~Object();    
    static void db_connect(Object *, const char *, Object *, const char *);    
    void testSignal();    
db_signals:    
    void sig1();    
public db_slots:    
    void slot1();    
friend class MetaObject;    
private:    
     ConnectionMap connections;    
};

引入元对象系统

首先定义自己的信号和槽

  • 为了和普通成员进行区别(以使得预处理器可以知道如何提取信息),我们需要创造一些"关键字"

    • db_signals
    • db_slots
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      class Object
      {
      public:
          Object();
          virtual ~Object();
      db_signals:
          void sig1();
      public db_slots:
          void slot1();
      };
      
  • 通过自己的预处理器,将信息提取取来,放置到一个单独的文件中(比如db_object.cpp):
  • 规则很简单,将信号和槽的名字提取出来,放到字符串中。可以有多个信号或槽,按顺序"sig1\nsig2\n"
    1
    2
    static const char sig_names[] = "sig1\n";
    static const char slts_names[] = "slot1\n";
    
  • 这些信号和槽的信息,如何才能与类建立关联,如何被访问呢?

我们可以定义一个类,来存放信息:

1
2
3
4
5
struct MetaObject
{
    const char * sig_names;
    const char * slts_names;
};

然后将其作为一个Object的静态成员(注意哦,这就是我们的元对象啦 ):

1
2
3
4
class Object
{
    static MetaObject meta;
...

这样一来,我们的预处理器可以生成这样的 db_object.cpp 文件:

1
2
3
4
5
#include "object.h"

static const char sig_names[] = "sig1\n";
static const char slts_names[] = "slot1\n";
MetaObject Object::meta = {sig_names, slts_names};

信息提取的问题解决了:可是,还有一个严重问题,我们定义的关键字 C++ 编译器不认识啊,怎么办?

呵呵,好办,通过定义一下宏,问题是不是解决了:

1
2
# define db_slots
# define db_signals protected

建立信号槽链接

我们的最终目的就是:当信号被触发的时候,能找到并触发相应的槽。所以有了信号和槽的信息,我们就可以建立信号和槽的连接了。我们通过 db_connect 将信号和槽的对应关系保存到一个 mutlimap 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Connection
{
    Object * receiver;
    int method;
};

class Object
{
public:
...
    static void db_connect(Object*, const char*, Object*, const char*);
...
private:
    std::multimap<int, Connection> connections;

上面应该不需要什么解释了,我们直接看看db_connect该怎么写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
    int sig_idx = find_string(sender->meta.sig_names, sig);
    int slt_idx = find_string(receiver->meta.slts_names, slt);
    if (sig_idx == -1 || slt_idx == -1) {
        perror("signal or slot not found!");
    } else {
        Connection c = {receiver, slt_idx};
        sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
    }
}

首先从元对象信息中查找信号和槽的名字是否存在,如果存在,则将信号的索引和接收者的信息存入信号发送者的的一个map中。如果信号或槽无效,就什么都不用做了。

我们这儿定义了一个find_string函数,就是个简单的字符串查找(此处就不列出了)。

信号的激活

连接信息有了,我们看看信号到底是怎么发出的。

在 Qt 中,我们都知道用 emit 来发射信号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Object
{
public:
    void testSignal()
...
};

void Object::testSignal()
{
    db_emit sig1();
}

这儿 db_emit 是神马东西?C++编译器不认识啊,没关系,看仔细喽,加一行就行了

1
#define db_emit

从前面我的Object定义中可以看到,所谓的信号或槽,都只是普普通通的C++类的成员函数。既然是成员函数,就需要函数定义:

  • 槽函数:由于它包含我们需要的功能代码,我们都会想到在 object.cpp 文件中去定义它,不存在问题。
  • 信号函数:它的函数体不需要自己编写。那么它在哪儿呢?这就是本节的内容了

信号函数由我们的"预处理器"来生成,也就是它要定义在我们的 db_object.cpp 文件中:

1
2
3
4
void Object::sig1()
{
    MetaObject::active(this, 0);
}

我们预处理源文件时,就知道它是第几个信号。所以根据它的索引去调用和它关联的槽即可。具体工作交给了MetaObject类:

1
2
3
4
5
6
7
8
class Object;
struct MetaObject
{
    const char * sig_names;
    const char * slts_names;

    static void active(Object * sender, int idx);
};

这个函数该怎么写呢:思路很简单

  • 从前面的保存连接的map中,找出与该信号关联的对象和槽
  • 调用该对象这个槽
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    typedef std::multimap<int, Connection> ConnectionMap;
    typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
    
    void MetaObject::active(Object* sender, int idx)
    {
        ConnectionMapIt it;
        std::pair<ConnectionMapIt, ConnectionMapIt> ret;
        ret = sender->connections.equal_range(idx);
        for (it=ret.first; it!=ret.second; ++it) {
            Connection c = (*it).second;
            //c.receiver->metacall(c.method);
        }
    }
    

补遗:

槽的调用

这个最后一个关键问题了,槽函数如何根据一个索引值进行调用。

  • 直接调用槽函数我们都知道了,就一个普通函数
  • 可现在通过索引调用了,那么我们必须定义一个接口函数
    1
    2
    3
    4
    class Object
    {
        void metacall(int idx);
    ...
    

该函数如何实现呢?这个又回到我们的元对象预处理过程中了,因为在预处理的过程,我们能将槽的索引和槽的调用关联起来。

所以,在预处理生成的文件(db_object.cpp)中,我们很容易生成其定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void Object::metacall(int idx)
{
    switch (idx) {
        case 0:
            slot1();
            break;
        default:
            break;
    };
}

至此,我们已经实现的一个简化的自己的信号与槽的程序。下面我们总体上看看程序的所有代码: 全家福

  • 类定义文件 object.h
     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
    #ifndef DB_OBJECT  
    #define DB_OBJECT  
    #include <map>  
    # define db_slots  
    # define db_signals protected  
    # define db_emit  
    class Object;  
    struct MetaObject  
    {  
        const char * sig_names;  
        const char * slts_names;  
        static void active(Object * sender, int idx);  
    };  
    struct Connection  
    {  
        Object * receiver;  
        int method;  
    };  
    typedef std::multimap<int, Connection> ConnectionMap;  
    typedef std::multimap<int, Connection>::iterator ConnectionMapIt;  
    class Object  
    {  
        static MetaObject meta;  
        void metacall(int idx);  
    public:  
        Object();  
        virtual ~Object();  
        static void db_connect(Object*, const char*, Object*, const char*);  
        void testSignal();  
    db_signals:  
        void sig1();  
    public db_slots:  
        void slot1();  
    friend class MetaObject;  
    private:  
         ConnectionMap connections;  
    };  
    #endif
    
  • 类实现文件 object.cpp
     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
    #include <stdio.h>  
    #include <string.h>  
    #include "object.h"  
    Object::Object()  
    {  
    }  
    Object::~Object()  
    {  
    }  
    static int find_string(const char * str, const char * substr)  
    {  
        if (strlen(str) < strlen(substr))  
            return -1;  
        int idx = 0;  
        int len = strlen(substr);  
        bool start = true;  
        const char * pos = str;  
        while (*pos) {  
            if (start && !strncmp(pos, substr, len) && pos[len]=='\n')  
                return idx;  
            start = false;  
            if (*pos == '\n') {  
                idx++;  
                start = true;  
            }  
            pos++;  
        }  
        return -1;  
    }  
    void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)  
    {  
        int sig_idx = find_string(sender->meta.sig_names, sig);  
        int slt_idx = find_string(receiver->meta.slts_names, slt);  
        if (sig_idx == -1 || slt_idx == -1) {  
            perror("signal or slot not found!");  
        } else {  
            Connection c = {receiver, slt_idx};  
            sender->connections.insert(std::pair<int, Connection>(sig_idx, c));  
        }  
    }  
    void Object::slot1()  
    {  
        printf("hello dbzhang800!");  
    }  
    void MetaObject::active(Object* sender, int idx)  
    {  
        ConnectionMapIt it;  
        std::pair<ConnectionMapIt, ConnectionMapIt> ret;  
        ret = sender->connections.equal_range(idx);  
        for (it=ret.first; it!=ret.second; ++it) {  
            Connection c = (*it).second;  
            c.receiver->metacall(c.method);  
        }  
    }  
    void Object::testSignal()  
    {  
        db_emit sig1();  
    }
    
  • 我们自己的预处理需要生成这样一个文件 db_object.cpp
  • 注意看这个文件:其实内容非常简单
    • 将信号和槽的信息存放到字符串中 ==>按顺序排放,所以有了索引值
    • 信号发射 其实就是 信号函数==> 信号的索引
    • metacall 其实就是 槽的索引==> 槽函数
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      #include "object.h"  
      static const char sig_names[] = "sig1\n";  
      static const char slts_names[] = "slot1\n";  
      MetaObject Object::meta = {sig_names, slts_names};  
      void Object::sig1()  
      {  
          MetaObject::active(this, 0);  
      }  
      void Object::metacall(int idx)  
      {  
          switch (idx) {  
              case 0:  
                  slot1();  
                  break;  
              default:  
                  break;  
          };  
      }
      
  • 最后,我们可以写一个小小的例子main.cpp :
    1
    2
    3
    4
    5
    6
    7
    8
    #include "object.h"  
    int main()  
    {  
        Object obj1, obj2;  
        Object::db_connect(&obj1, "sig1", &obj2, "slot1");  
        obj1.testSignal();  
        return 0;;  
    }
    
  • 程序的编译就不用多说了,用你熟悉的msvc或者g++
    1
    2
    cl main.cpp object.cpp db_object.cpp -o dbzhang800
    g++ main.cpp object.cpp db_object.cpp -o dbzhang800
    

零零散散,写在后面

我不确定是不是已经元对象系统和信号槽最基本的概念表达清楚了。反正我想,如果你对Qt感兴趣,相对Qt的信号和槽进一步的了解,但是目前尚对阅读Qt的源码觉得无比恐怖,本文可能会对你有帮助。

文中将东西精简到我个人能做到的极限了,所以有很多很多没提到的东西:

Q_OBJECT

用Qt,我们都知道这个宏,可是我们前面压根没提。因为我怕打乱思路,这儿补上吧。我的前面的代码可以替换为:

1
2
3
4
5
# define DB_OBJECT static MetaObject meta; void metacall(int idx);

class Object
{
   DB_OBJECT

DB_OBJECT 还可以作为一个标记:如果我们写好了自己的类似于Qt中的moc的预处理器,如何判断一个文件是否需要预处理来生成 db_object.cpp 文件呢?此时就可以根据类定义中是否有宏来判断。

题外: 为什么添加宏后会容易遇到链接错误?你能看到原因么?因为它展开后就是类的成员,可是其定义要通过预处理进行生成。如果你没有运行预处理器,也就没有 db_object.cpp 这种文件,肯定要出错了。

Connection

我们前面在Connection只保存了接收者的指针和槽的索引,我们可以保存更多一点的信息的:可以看看Qt保存了哪些东西

1
2
3
4
5
6
7
QObjectPrivate::Connection *c = new QObjectPrivate::Connection; 
    c->sender = s; 
    c->receiver = r; 
    c->method = method_index; 
    c->connectionType = type; 
    c->argumentTypes = types; 
    c->nextConnectionList = 0;

应该很容易看懂,不做解释了。

Qt中信号和槽主要有直接连接和队列连接两种方式,我们这儿只提到了前者,后者和Qt的事件系统搅和在一起。只要搞清楚了Qt事件系统,就会发现和直接连接没有什么区别了。 其他

信号和槽的参数

这个,例子中举的都是信号和槽都是无参数的例子。加上参数,尽管概念上没变化,但复杂度就大大提高了。所以本文对此不想涉及,也没必要吧,直接去看Qt的源码吧。

信号和信号连接

信号和槽一样,都可以被调用,本例进行扩展也很容易,需要metacall那个函数,以及信号和槽要加个区别的标记(回到最前面不妨看看Qt的SLOT和SIGNAL究竟是神马东西)。

派生

本文中只涉及到一个类,如何在该类的基础上进行派生呢? 个人能力有限,例子中没考虑这个问题。

...

好了,忙到下午终于把昨天冒出来的这个想法付诸实施了,希望五一之后,生活会精彩一点。dbzhang800 2011.04.30

注:本文最早发布在CSDN,相关内容以后会逐步转移到blog.debao.me中。dbzhang800 2015.04.29

Qt Qt4, C++, Qt