不要被这个标题唬住了,实际上我是非常认可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. 深入理解python中可迭代对象,迭代器,生成器

    英文原文出处:Iterables vs. Iterators vs. Generators 在python学习中,通常会陷入对以下几个相关概念之间的确切差异的困惑中: a container(容器) ...

  2. Redis 占用Windows系统盘空间23G

    Redis常出现问题总结: 1.当出现修改--maxheap and --heapdir 在启动这两个版本时都会创建一个 RedisQFork.dat文件,我不确定 RedisQFork 文件是否变小 ...

  3. 动态往 datagridview 追加行、列操作

    DataGridViewTextBoxColumn acCode = new DataGridViewTextBoxColumn(); acCode.Name = "acCode" ...

  4. 关于PHP中的Trait

    今天看PHP框架,看到Trait部分.没见过,好奇查了一下. PHP手册说的是解决多继承的问题.但是一般面向对象的语言中,解决多继承都是通过接口,PHP也有接口.貌似看上去Trait和Interfac ...

  5. 842. Split Array into Fibonacci Sequence

    Given a string S of digits, such as S = "123456579", we can split it into a Fibonacci-like ...

  6. java中容器的概念

    容器:顾名思义,装东西的器物至于spring中bean,aop,ioc等一些都只是实现的方式具体容器哪些值得我们借鉴,我个人觉得是封装的思想.将你一个独立的系统功能放到一个容器之中,可以当做一个大的接 ...

  7. centos6.5 git clone http 报错

    自己搭建服务器环境为centos6.5,需要使用git clone 命令的时候报错 首先查看centos上安装的git版本,我的版本为1.7.10 报错后,查阅相关资料需将centos升级,操作如下 ...

  8. shell-003:用for循环统计内存使用量

    shell-100主要是用于练习! #!/bin/bash # 统计内存的使用量(这里用ps统计) # 第一步:不打印第一行,这里的sed ‘1d’ 去掉 for n in `ps aux |sed ...

  9. VIA格式转COCO格式

    VIA是一款很好用的标注软件,基于网页,不过现在开源的大多数目标检测器都是基于COCO训练和测试的,我们如果想要训练自己的数据集,要么修改源代码,要么将自己的标注格式改成COCO格式,采用第一种方法很 ...

  10. flask简单了解

    Flask简介: Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务,在介绍Flask之前首先来聊下它和Django的联系以及区别,djang ...