不要被这个标题唬住了,实际上我是非常认可Qt的。在C++实现的开源产品中没有哪一个的API风格比得上Qt,拥有高度一致性,符合常识,符合直觉,几乎不用学就可以直接上手。或许是由于我们摆脱不了马太效应的控制,赞誉已经给到了Qt的缺陷。Qt的最大问题就是提供了uic和moc。界面设计器生成xml文件,需要uic编译它生成C++代码;组织界面的C++代码其实一点都不复杂,完全可以由界面设计器直接生成。可以给Qt找到需要uic的理由——实现了分工,可以并行,为设计器开发团队屏蔽了C++语法的复杂性。然而,uic相对于界面设计器来说,工作量几乎可以忽略不记,在管理实践上如此不平衡的分工没有任何意义,并行也就说不过去了。组织界面的C++代码完全掌控在Qt团队手里,完全可以用最简单的方式实现(uic生成的c++代码也确实非常简单),这样一来也没有什么“C++语法的复杂性”需要屏蔽了。如果uic对用户来说没有坏处,仅仅给Qt团队增加了工作量,也就无可非议了。但是,uic集成到第三方开发工具中时,导致设计器创建的界面,不能及时生成为C++代码,必须手动执行一下uic。

uic跟moc比起来,就是小巫见大巫了。提供moc的原因,很大一部分是因为信号和槽机制。每每听到有人带着无比崇敬的态度布道Qt的信号和槽机制,真希望他们能知道信号和槽到底为了什么而存在。还是先来看一段Qt的代码吧。

class QDataSourceWidget : public QTreeView
{
Q_OBJECT
public:
explicit QDataSourceWidget(QWidget *parent = 0);
~QDataSourceWidget(); signals:
void LayerAdded(IMapPtr, ILayerPtr, ILayerProviderPtr); protected:
virtual void LayerAddEvent(IMapPtr map, ILayerPtr layer, ILayerProviderPtr provider)
{
emit LayerAdded(map,layer,layerProvider);
} private slots:
void NodeDoubleClicked(const QModelIndex &index)
{
...
LayerAddEvent(map,layer,provider);
}
};

这段代码要完成这样一个功能:当表示数据源的QTreeView的节点双击时,打开数据,为数据创建一个可视化图层添加到map中,然后对外发布一个已经添加新图层的消息。很简单的一个功能,看看为了实现它Qt提供了些什么?4个扩展关键字——Q_OBJECT、signals、slots、emit;3个需要注册到QMetaTypes的自定义类型IMapPtr、ILayerPtr、ILayerProviderPtr(这3种类型实际上是另外3种类型的指针,但是必须得typedef才能注册到QMetaTypes中);1个元编译器moc。

非常代价高昂的解决方案,连编译器这种重型武器都上场了。为什么需要编译器?这段代码已经不可以再被称作C++了,就像.NET平台下的c++ cli一样,已经基于C++扩展出了一门新语言。众所周知C++的编译器非常难写,通常新标准发布10年之后都不被完全支持,这跟C++语义重载过多、语法自相矛盾、机制过于复杂不无关系。元编译器没有直接生成机器码,而是将“Qt c++”编译成了能够实现信号和槽机制的标准C++,再由C++编译器编译成机器码。这种方案确实避免了面对不同架构不同版本CPU的麻烦,但是仍然需要面对“Qt c++”中的C++成分。这就是偶尔会遇到元编译器执行失败、元编译器生成的C++代码编译不过的原因。这些现象在新版本Qt中确有很大改观,但是C++标准委员会并没有浪子回头的意思,元编译器即将面对的是更多语义重载、更多语法矛盾、更复杂的机制。

其实“Qt c++”也没怎么扩展标准C++,就多了4个关键字而已,而这4个关键字就是要派重武器——编译器——上场的罪魁祸首之一。这4个关键字还起了另外一个坏作用,让针对标准C++的代码自动格式化、代码自动完成失效了。对于需要注册QMetaTypes倒没有什么好抱怨的,毕竟带来了其它好处。

那么Qt以如此之高代价实现的信号和槽机制到底是什么高档玩意呢?说白了就是一种发布/订阅机制而已,对于没有从语言层面上支持调用栈上的发布/订阅机制的编程语言来说,一般通过两种方式来实现——Listener模式和回调函数。java swing就是典型的Listener模式,这个很显然;如果说MFC的Message Map是回调函数,可能会遇到争议。Message Map提供了消息码到消息处理函数指针的映射,消息循环从Message Map中查找到处理某个消息的所有函数指针,然后依次调用。消息循环是框架提供的,只是通过Message Map的形式传进去一个函数指针而已,虽然没有直接调用SetXXXCallBack,不影响它仍然是回调函数。

Qt为何弃这两种方式不用呢?确实有说得过去的理由。首先,C++没有匿名类,没有垃圾回收机制。如果采用Listener模式,必须得为每个不同签名的消息至少定义一个类,必须得合理地管理这些Listener的生命周期。MFC的Message Map方式,需要在代码中写很多宏,在不考虑代码自动完成时,显然只写下signals和slots两个关键字更为方便。从而,Qt便仓促地选择了由GTK发明的信号和槽的概念。(注:这是笔者帮Qt想的理由,是否还有其它理由笔者没有深入了解。)MFC的Message Map还是有他的拥趸的,wxWidgets便是其中较为知名的一员。

其实完全可以通过C++实现比Message Map更好的回调机制。在提出实现方式之前先明确一下需求和约束。

首先是需求,第一,订阅方可以是成员函数、静态函数、C函数、仿函数;第二,发布方可以支持多个订阅者同时订阅。如果满足这两条需求就已经比Qt的信号和槽机制要强大了。

当然也有一些约束,第一,既然发布方可以支持多个订阅者同时订阅,那么发布方若要采纳订阅者的返回值的话到底应该采纳哪一个的,这是个问题,所以干脆让订阅者全都返回void(Qt目前支持返回非void类型,但是有什么卵用他们内部仍然有争议);第二,轻量级,不用stl,不用boost(不用boost还说得过去,stl毕竟是c++的标准库。我有我的理由,C++的缺陷导致编译器特别难写,可以说在模板方面找不到实现地完全正确的编译器。一些编译器不能正确的为静态的或者全局的模板类变量生成构造代码,这应该是Google代码规范禁止这么做的原因,全局的和静态的类只能声明为指针,由程序员确保其被正确地构造出来。)。加上这两条约束,仍然不影响满足前两条需求的发布/订阅机制比Qt的信号和槽机制强大。

接下来给出完全通过C++实现的比Message Map和信号/槽机制更强大更轻量级的回调实现机制。

首先给出返回值是void类型可以代表成员函数、静态函数、C函数、仿函数的订阅者接口定义。这里用到了C++11的可变模板参数机制,只是为了方便而已。要支持C++98,可以用typelist机制或者直接多定义几个不同参数数量的模板。推荐用后者,typelist可能有些编译器支持不了。

template<typename ...Args>
struct IEventHandler
{
virtual void operator()(Args&... args) = 0; IEventHandler() {};
virtual ~IEventHandler() {}; private:
IEventHandler(const IEventHandler &) = delete;
IEventHandler &operator=(const IEventHandler &) = delete;
};

接下来支持静态函数、C函数、仿函数的订阅者实现。

template<typename Callable, typename ...Args>
class CallableEventHandler : public IEventHandler<Args...>
{
public:
CallableEventHandler(Callable handler)
{
_handler = handler;
} virtual ~CallableEventHandler() {}; public:
void operator()(Args&... args)
{
_handler(args...);
} private:
Callable _handler;
};

然后,支持成员函数的订阅者实现。

template<typename T, typename ...Args>
class EventHandler : public IEventHandler<Args...>
{
public:
typedef void(T::*Handler)(Args...);
EventHandler(T* receiver, Handler handler)
{
_receiver = receiver;
_handler = handler;
} virtual ~EventHandler() {}; public:
void operator()(Args&... args)
{
(_receiver->*_handler)(args...);
} private:
Handler _handler;
T* _receiver;
};

最后,发布方实现。

template<typename ...Args>
class Event
{
public:
typedef IEventHandler<Args...>* Callable; public:
Event()
{
_valid = false;
_event = nullptr;
}; Event(const Callable& h)
{
_handler = h;
_valid = true;
_event = nullptr;
} ~Event()
{
if (_event != nullptr)
{
delete _event;
}
}; const Event<Args...>& operator = (const Callable& h)
{
_handler = h;
_valid = true;
if (_event != nullptr)
{
delete _event;
_event = nullptr;
} return *this;
} Event(const Event<Args...> & e)
{
this->Add(e);
} Event<Args...> &operator=(const Event<Args...> & e)
{
_valid = false;
if (_event != nullptr)
{
delete _event;
_event = nullptr;
} this->Add(e); return *this;
} public:
void Raise(Args&... args)
{
if (_valid)
{
(*_handler)(args...);
}
if (_event != nullptr)
{
_event->Raise(args...);
}
} void operator()(Args&... args)
{
this->Raise(args...);
} public:
void Add(const Callable& h)
{
if (_valid)
{
if (_event != nullptr)
{
_event->Add(h);
}
else
{
_event = new Event<Args...>(h);
}
}
else
{
_handler = h;
_valid = true;
}
} void Remove(const Callable& h)
{
if (_valid && _handler == h)
{
if (_event == nullptr)
{
_valid = false;
}
else
{
Event<Args...>* event_ = _event;
_valid = _event->_valid;
_handler = _event->_handler;
_event = _event->_event; event_->_event = nullptr;
delete event_;
}
}
else if (_event != nullptr)
{
_event->Remove(h);
}
} void Add(const Event<Args...>& e)
{
Event<Args...>* event_ = const_cast<Event<Args...>*>(&e);
while (event_ != nullptr)
{
if (event_->_valid)
{
this->Add(event_->_handler);
}
event_ = event_->_event;
}
} void Remove(const Event<Args...>& e)
{
Event<Args...>* event_ = const_cast<Event<Args...>*>(&e);
while (event_ != nullptr)
{
if (event_->_valid)
{
this->Remove(event_->_handler);
}
event_ = event_->_event;
}
} public: const Event<Args...>& operator += (const Callable& h)
{
this->Add(h); return *this;
} const Event<Args...>& operator -= (const Callable& h)
{
this->Remove(h); return *this;
} const Event<Args...>& operator += (const Event<Args...>& e)
{
this->Add(e); return *this;
} const Event<Args...>& operator -= (const Event<Args...>& e)
{
this->Remove(e); return *this;
} private:
bool _valid;
Callable _handler;
Event<Args...>* _event;
};

仅此而已,加上很多空白行才有不到300行代码。这是我在开源项目tGis实现的发布/订阅机制,可以采用如下方式使用。

Event SomeEvent;
EventHandler handler;
SomeEvent += &handler; // 绑定订阅者到发布者方式一
SomeEvent.Add(&handler); // 绑定订阅者到发布者方式二
SomeEvent += new EventHandler; // 要绑定订阅者到发布者方式三,暂不支持,会导致内存泄漏 SomeEvent(); // 触发事件方式一
SomeEvent.Raise(); //触发事件方式二

当然,这个实现方式仍然有些不足。第一,参数不能是右值;触发事件的函数参数不能直接作为事件的参数,不能在事件参数上直接构造对象,而需要先声明个变量接收一下。这个不足解决起来也很简单,加个接收右值参数的重载就行了。(这个不足只是对C++11而言的)

Event SomeEvent;
SomeEvent(SomeClass()); // C++11中,这样触发事件是错误的,编译不过
SomeClass some;
SomeEvent(some); // 这样一定是可以的

第二,对订阅者进行了包装,但是没有提供生命周期管理机制。这只是需求和代码量的权衡,我不想就为支持“+= new”式的语法搞出一套生命周期管理机制来。简单的实现方式是auto_ptr,如果愿意,读者可以自己实现。在这里,我要善意的提醒一句,c++里的堆是开发库提供的,很可能不同的链接库以及执行文件中的堆不是一个堆,用一个堆的delete去删除另一个堆里的对象将会收获一个程序崩溃

希望这里提到的缺陷不要影响到读者的技术选型。优势就是优势,缺陷就是缺陷;不能优势大于缺陷之后缺陷也成为了优势,技术人应该有客观的技术态度。要知道我是在用Qt的,参考开源项目tGis。其实信号和槽机制没有给用户带来什么麻烦,仅仅是给Qt团队带来了巨大的麻烦而已。

信号和槽:Qt中最差劲的创造的更多相关文章

  1. QT 信号与槽 QT简单加法器的实现

    信号与槽 背景: 面向过程 模块之间低耦合设计(高内聚). 函数调用: 直接调用 回调调用(低耦合) 面向对象 模块之间低耦合设计(高内聚) 对象调用 直接调用 接口调用 QT: 信号与槽解决问题: ...

  2. Qt Quick 事件处理之信号与槽

    前面两篇文章<QML 语言基础>和<Qt Quick 简单教程>中我们介绍了 QML 语言的基本的语法和 Qt Quick 的常见元素,亲们,通过这两篇文章,您应该已经能够完毕 ...

  3. 2.QT-窗口组件(QWidget),QT坐标系统,初探消息处理(信号与槽)

    本章主要内容如下: 1) 窗口组件(QWidget) 2) QT坐标系统 3) 消息处理(信号与槽) 窗口组件(QWidget) 介绍 Qt以组件对象的方式构建图形用户界面 Qt中没有父组件的顶级组件 ...

  4. Qt学习之信号与槽(一)

    Qt学习之信号与槽(一) 目录 QT的信号与槽机制 在窗口的UI设计中操作添加信号和槽     QT的信号与槽机制   QT的两种机制 在Qt和PyQt中有两种通信机制: 低级事件处理机制(low-l ...

  5. Qt Quick 事件处理之信号与槽(foruok的博客)

    前面两篇文章<QML 语言基础>和<Qt Quick 简单教程>中我们介绍了 QML 语言的基本语法和 Qt Quick 的常见元素,亲们,通过这两篇文章,您应该已经可以完成简 ...

  6. QT(4)信号与槽

    mainWidget.h #ifndef MAINWIDGET_H #define MAINWIDGET_H #include <QWidget> #include <QPushBu ...

  7. Qt中的QWebView

    一.Webkit了解   Webkit是一个开源的浏览器引擎,chrome也使用了作为核心.Qt中对Webkit做了封装,主要有以下几个类: QWebView :最常用的类,作为一个窗体控件 QWeb ...

  8. PyQT5:信号和槽

    PyQT5:信号和槽 信号和槽 Qt的主要特征之一是它使用信号和插槽在对象之间进行通信. 当潜在的事件发生时,会发出一个信号.插槽是可调用的Python,如果将信号连接到插槽,则在发出信号时将调用该插 ...

  9. qt中信号与槽机制

    一. 简介 就我个人来理解,信号槽机制与Windows下消息机制类似,消息机制是基于回调函数,Qt中用信号与槽来代替函数指针,使程序更安全简洁. 信号和槽机制是 Qt 的核心机制,可以让编程人员将互不 ...

随机推荐

  1. java 七牛上传图片到服务器(采用的html5 压缩 传输base64方式)

    //html 页面如下<div class="form-group"> <label class="col-sm-2 control-label&quo ...

  2. 以太坊系列之三: 以太坊的crypto模块--以太坊源码学习

    以太坊的crypto模块 该模块分为两个部分一个是实现sha3,一个是实现secp256k1(这也是比特币中使用的签名算法). 需要说明的是secp256k1有两种实现方式,一种是依赖libsecp2 ...

  3. 2018年第九届蓝桥杯C/C++A组省赛(最后一题)

    第十题 付账问题   [题目描述]    几个人一起出去吃饭是常有的事.但在结帐的时候,常常会出现一些争执.    现在有 n 个人出去吃饭,他们总共消费了 S 元.其中第 i 个人带了 ai 元.幸 ...

  4. #首行输入数n,接下来输入n行数,以空格隔开

    #首行输入数n,接下来输入n行数,以空格隔开 n = int(raw_input())# print nL = []for i in range(n): L.append([int(x) for x ...

  5. Squid代理服务器(一)——大家所用的游戏代理软件到底为何物?

    一.代理服务器应用场景分析 想当年大学时候宿舍控制网速,苦了我们这帮魔兽党,一到晚上工会活动我们就得和全校上万名学生抢网速,作为坦克的我总是因为网速问题导致团灭,咱也是有自尊的人,一怒之下花300元办 ...

  6. ionic中文教程[来自皓眸大前端]

    做前端的同学有福了,学完比较热火的angular,你就可以开始动手做静态的WebApp了,这是多么幸福的一件事啊.静态的WebApp,你可以做任何的Demo,甚至可以做一些通关小游戏这个先不谈.做完了 ...

  7. 从pg_hba.conf文件谈谈postgresql的连接认证

    最近一直在弄postgresql的东西,搭建postgresql数据库集群环境什么的.操作数据库少不得要从远程主机访问数据库环境,例如数据库管理员的远程管理数据库,远程的客户存取数据库文件. 而在po ...

  8. 模板【洛谷P3368】 【模板】树状数组 2

    P3368 [模板]树状数组 2 如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数数加上x 2.求出某一个数的值 树状数组区间加,单点查询. code: #include <i ...

  9. 关于 SimpleMembership 中 CreateDate 的问题

    使用 WebMatrix.WebData.WebSecurity.CreateUserAndAccount(model.UserName, model.Password,                ...

  10. 网络控制芯片AX88796B系列使用简介

    目录 1. 特性 2. 结构框图 3. 接收 3.1 缓存空间 3.2 Receiver Buffer Ring 3.3 接收机制 4. 发送 5. 编程过程简要说明 5.1 初始化配置 5.2 接收 ...