Qt为何使用Moc进行信号和槽操作?

模板是C++中的一种内置机制,它允许编译器根据传入的参数类型动态生成代码。因此,模板对框架创建者来说非常有趣,我们在Qt的许多地方都使用了高级模板。然而,也存在一些限制:有些事情你可以用模板轻松表达,而有些事情则不可能用模板表达。一个通用的向量容器类可以轻松表达,即使是针对指针类型的局部特化,而一个基于字符串的XML描述设置图形用户界面的函数就不可能用模板表达。然后还有一个灰色地带。为了代码大小、可读性、可移植性、可用性、扩展性、健壮性和最终的设计美感而“修改”模板中的事情。模板和C预处理器都可以被拉用到完成极其聪明和令人眼花缭乱的事情。但是,仅仅因为可以完成这些事情,并不意味着完成这些事情是正确的设计选择。不幸的是,代码并非为了在书中发表,而是为了编译器在现实世界操作系统上进行编译。

以下是Qt使用moc的原因之一

语法很重要

语法不仅仅是糖:我们用来表达算法的语法可以显著影响代码的可读性和可维护性。Qt的信号和槽所使用的语法在实践中已经证明非常成功。语法直观,使用简单,易于阅读。学习Qt的人发现语法有助于他们理解和利用信号和插槽的概念 - 尽管它具有高度抽象和通用的性质。这有助于程序员从一开始就设计正确,甚至不需要考虑设计模式。

代码生成器是好的

Qt的moc(元对象编译器)提供了一种超越编译语言设施清洁的方法。它通过生成可由任何标准C++编译器编译的额外C++代码来实现。当moc读取C++源文件时。如果它发现一个或多个包含Q_OBJECT宏的类声明,它会生成另一个C++源文件,其中包含那些类的元对象代码。由moc生成的C++源文件必须与类的实现编译和链接(或可以将其#included到类的源文件中)。通常,不手动调用moc,而是由构建系统自动调用,因此不需要程序员做额外的工作。

Qt使用的代码生成器并不仅限于moc。另一个著名的例子是uic(用户界面编译器)。它接受XML格式的用户界面描述,并创建设置表单的C++代码。在Qt之外,代码生成器也十分普遍。例如,rpcidl,它们能够使程序或对象在不同进程或机器之间进行通信。还有各种扫描器和解析器生成器,其中lexyacc是最知名的两个。它们接受语法规范作为输入,生成实现状态机的代码。代码生成器的替代品可以是黑客编译器、专有语言或者带有单向对话框或向导的图形化编程工具,这些工具在设计和编译时生成难以理解的代码。我们并不是将客户提供给专有的C++编译器或特定的集成开发环境,而是让他们可以使用他们喜欢的任何工具。我们并不是强迫程序员把生成的代码加入源代码库,而是鼓励他们将我们的工具添加到构建系统中:更干净、更安全,更符合UNIX的精神。

图形用户界面是动态的

C++是一种标准化、强大而详尽的多用途语言。它是唯一一种在如此广泛的软件项目中得到应用的编程语言,从整个操作系统、数据库服务器到高端图形应用,再到常见的桌面应用,一应俱全。C++取得成功的一个关键因素是其具有可扩展的语言设计,它着眼于最大性能和最小内存消耗,同时仍然维护与ANSI C的兼容性。

尽管具有这些优势,但也存在一些劣势。对于C++而言,静态对象模型与Objective C的基于动态消息的方法相比,在基于组件的图形用户界面程序设计时存在明显劣势。对高端数据库服务器或操作系统的好处并不一定是GUI前端的最佳设计选择。有了moc,我们将这种劣势转变为优势,并添加了满足安全高效图形用户界面编程挑战所需的灵活性。

我们的方法远远超越了使用模板所能做到的事情。例如,我们可以有对象属性。我们还可以有重载的信号和槽,这在编程语言中是一个关键概念时,感觉自然。我们的信号不会增加类实例的大小,这意味着我们可以在不破坏二进制兼容性的情况下添加新的信号。

另一个好处是我们可以在运行时探索对象的信号和槽。我们可以使用类型安全的命名调用建立连接,而无需知道我们连接的对象的确切类型。这是基于模板的解决方案难以做到的。这种类型的运行时内省开启了新的可能性,例如由Qt Designer的XML UI文件生成的并连接的GUI。

调用性能并不是一切

Qt的信号和槽实现并不像基于模板的解决方案那样快。在发射一个信号的过程中,大约相当于四个普通函数调用的成本,而Qt需要相当于大约十个函数调用的努力。这并不奇怪,因为Qt的机制包括一个通用的封装器、反射、不同线程之间的队列调用,以及最终的脚本能力。它不依赖于过度的内联和代码膨胀,并提供了无与伦比的运行时安全性。Qt的迭代器是安全的,而基于更快模板的系统的迭代器则不是。即使是在向多个接收者发射信号的流程中,这些接收者也可以安全地被删除,而不会使你的程序崩溃。如果没有这种安全性,你的应用程序最终会因为难以调试的释放内存读取或写入错误而崩溃。

然而,基于模板的解决方案是否可以提高使用信号和槽的应用程序的性能?虽然Qt确实会增加通过信号调用槽的成本,但这种调用成本仅占槽成本的一小部分。与Qt的信号和槽系统进行基准测试通常是使用空槽。一旦你在槽中执行任何有用的操作,例如一些简单的字符串操作,调用开销就变得可以忽略不计。Qt的系统如此优化,以至于任何需要operator new或delete(例如字符串操作或在模板容器中插入/删除)的操作,其成本都比发射信号要高得多。

另外:如果你在一个性能关键任务的内循环中有一个信号和槽的连接,并且你将其识别为瓶颈,那么考虑使用标准的监听器接口模式而不是信号和槽。在这种情况下,你可能只需要1:1的连接。例如,如果你有一个从网络下载数据的对象,使用一个信号表示请求的数据到达是非常合理的。但是,如果你需要逐个字节将数据发送给消费者,那么使用监听器接口而不是信号和槽会更合适。

无限制

由于我们有了信号和槽的moc,我们可以在其中添加其他有用的事情,这些事情无法通过模板完成。其中包括通过生成的tr()函数进行的范围翻译,以及具有反射和扩展运行时类型信息的先进属性系统。仅属性系统本身就具有很大的优势:一个像Qt设计师

GUIs are Dynamic

这样的强大和通用用户界面设计工具,如果没有这样一个强大而反射的属性系统,就很难(如果不是不可能的)编写。但不仅如此。我们还提供了一个动态的qobject_cast<T>()机制,它不依赖于系统的RTTI,因此不受其限制。我们用它来安全地从动态加载的组件查询接口。另一个应用领域是动态元对象。例如,我们可以创建围绕ActiveX组件的元对象。或者我们可以通过导出其元对象来将Qt组件导出为ActiveX组件。使用模板无法做到这两者中的任何一项。

C++配合moc几乎为我们提供了Objective-C或Java运行时环境的灵活性,同时保持了C++特有的性能和可扩展性优势。这就是Qt成为我们今天具有弹性和舒适性的工具的原因。

© 2024 The Qt Company Ltd. 本文档的贡献是各自所有者的版权。本提供的文档是根据GNU自由文档许可证的第1.3版发布的条款授予的,由自由软件基金会发布。Qt及其相关标志是芬兰和其他国家/地区的The Qt Company Ltd.的注册商标。所有其他商标均为各自所有者的财产。