为什么 Qt 使用 Moc 来处理信号和插槽?

模板是 C++ 中的一种内置机制,它允许编译器根据传递的参数类型即时生成代码。因此,模板对于框架 Creator 来说非常有趣,我们也确实在 Qt 的许多地方使用了高级模板。不过,模板也有局限性:有些东西可以用模板轻松表达,有些东西则无法用模板表达。一个通用的向量容器类很容易表达,即使对指针类型进行部分特化也是如此,而一个根据以字符串形式给出的 XML 描述来设置图形用户界面的函数却不能用模板来表达。在这两者之间还有一个灰色地带。你可以用模板来黑掉一些东西,但代价是代码大小、可读性、可移植性、可用性、可扩展性、健壮性以及最终的设计美感。模板和 C 预处理器都可以扩展到令人难以置信的智能和令人匪夷所思的功能。但是,仅仅因为可以做这些事情,并不一定意味着做这些事情就是正确的设计选择。不幸的是,代码并不是用来发表在书本上的,而是在现实世界的操作系统上用现实世界的编译器编译出来的。

以下是 Qt 使用 moc 的一些原因:

语法至关重要

语法不只是糖:我们用来表达算法的语法会极大地影响代码的可读性和可维护性。实践证明,Qt 信号和插槽使用的语法非常成功。该语法直观、简单易用、易于阅读。学习 Qt 的人发现,尽管信号和槽的概念非常抽象和通用,但该语法有助于他们理解和使用信号和槽的概念。这有助于程序员从一开始就能正确地进行设计,甚至无需考虑设计模式。

代码生成器很好

Qt 的moc (Meta Object Compiler,元对象编译器)提供了一种简洁的方法来超越编译语言的功能。它通过生成额外的 C++ 代码来实现这一功能,这些代码可由任何标准的 C++ 编译器编译。moc 读取 C++ 源文件。如果发现一个或多个类声明包含Q_OBJECT 宏,它就会生成另一个 C++ 源文件,其中包含这些类的元对象代码。moc 生成的 C++ 源文件必须与类的实现进行编译和链接(也可以#included 到类的源文件中)。通常情况下,moc 不是手动调用的,而是由构建系统自动调用的,因此程序员无需额外费力。

moc 并不是 Qt XML 使用的唯一代码生成器。另一个突出的例子是uic (User Interface Compiler)。它采用 XML 格式的用户界面描述,并创建用于设置表单的 C++ 代码。在 Qt 之外,代码生成器也很常见。例如rpcidl ,它们能让程序或对象跨越进程或机器边界进行通信。还有各种各样的扫描器和解析器生成器,其中最著名的是lexyacc 。它们将语法规范作为输入,生成实现状态机的代码。代码生成器的替代品是黑客编译器、专有语言或带有单向对话框或向导的图形编程工具,这些工具在设计阶段而非编译阶段生成晦涩难懂的代码。我们不会将客户锁定在专有的 C++ 编译器或特定的集成开发环境中,而是让他们能够使用自己喜欢的任何工具。我们不强迫程序员将生成的代码添加到源代码库中,而是鼓励他们将我们的工具添加到他们的构建系统中:更简洁、更安全、更符合 UNIX 的精神。

动态图形用户界面

C++ 是一种标准化的、强大的、精心设计的通用语言。从整个操作系统、数据库服务器、高端图形应用程序到普通的桌面应用程序,C++ 是唯一一种在如此广泛的软件项目中得到应用的语言。C++ 成功的关键之一是其可扩展的语言设计,在保持 ANSI C 兼容性的同时,注重最高的性能和最小的内存消耗。

虽然有这些优点,但也有一些缺点。就 C++ 而言,当涉及基于组件的图形用户界面编程时,静态对象模型与 Objective C 的动态消息传递方法相比明显处于劣势。适合高端数据库服务器或操作系统的设计并不一定适合图形用户界面前端。通过moc ,我们将这一劣势转化为优势,并增加了应对安全高效图形用户界面编程挑战所需的灵活性。

我们的方法远远超出了使用模板所能做到的一切。例如,我们可以拥有对象属性。我们还可以重载信号和插槽,这在以重载为关键概念的编程语言中感觉很自然。我们的信号在类实例的大小上添加的字节数为零,这意味着我们可以在不破坏二进制兼容性的情况下添加新的信号。

另一个好处是,我们可以在运行时探索对象的信号和插槽。我们可以使用类型安全的逐名调用建立连接,而无需知道所连接对象的确切类型。这在基于模板的解决方案中是不可能实现的。这种运行时自省开辟了新的可能性,例如从Qt Widgets Designer 的 XML UI 文件生成和连接图形用户界面。

调用性能并不代表一切

Qt 的信号和插槽实现不如基于模板的解决方案快。在普通模板实现中,发出一个信号的代价大约是调用四个普通函数,而 Qt 需要付出的代价大约是调用十个函数。这并不奇怪,因为 Qt 机制包括一个通用的 marshaller、内省、不同线程之间的队列调用以及最终的脚本性。它不依赖于过多的内联和代码扩展,并提供无与伦比的运行时安全性。Qt 的迭代器是安全的,而那些更快的基于模板的系统则不安全。即使在向多个接收器发送信号的过程中,也可以安全地删除这些接收器,而不会导致程序崩溃。如果没有这种安全性,你的程序最终会因难以调试的自由内存读写错误而崩溃。

然而,基于模板的解决方案难道不能提高使用信号和插槽的应用程序的性能吗?虽然 Qt 确实在通过信号调用插槽的成本上增加了少量开销,但调用成本只占插槽整个成本的一小部分。针对 Qt 信号和插槽系统的基准测试通常是在空插槽中进行的。只要在槽中执行任何有用的操作,例如一些简单的字符串操作,调用开销就变得微不足道了。Qt 的系统经过优化,任何需要操作符 new 或 delete 的操作(例如字符串操作或从模板容器中插入/删除某些内容)都比发出信号要昂贵得多。

题外话:如果在性能关键任务的紧凑内循环中存在信号和插槽连接,并且将此连接视为瓶颈,那么请考虑使用标准的监听器接口模式,而不是信号和插槽。在这种情况下,您可能只需要 1:1 的连接。例如,如果您有一个从网络下载数据的对象,那么使用信号来表示所请求的数据已到达是一个非常合理的设计。但如果需要将每个字节逐一发送给消费者,则应使用监听器接口,而不是信号和插槽。

没有限制

因为我们有了信号和插槽的moc ,所以我们还可以添加其他有用的东西,而这些是模板无法做到的。其中包括通过生成的tr() 函数进行范围翻译,以及带有内省和扩展运行时类型信息的高级属性系统。属性系统本身就是一个巨大的优势:如果没有强大的内省属性系统,像Qt Widgets Designer 这样强大的通用用户界面设计工具就很难编写,甚至不可能编写。但这并没有结束。我们还提供了一种动态的qobject_cast<T>()机制,它不依赖于系统的 RTTI,因此也没有其局限性。我们用它来安全地查询动态加载组件的接口。另一个应用领域是动态元对象。例如,我们可以使用 ActiveX 组件,并在运行时围绕它创建一个元对象。或者,我们可以通过导出元对象将 Qt 组件导出为 ActiveX 组件。而使用模板则无法做到这两点。

使用moc 的 C++ 本质上为我们提供了 Objective-C 或 Java Runtime Environment 的灵活性,同时保持了 C++ 独特的性能和可扩展性优势。正是它使 Qt 成为我们今天所拥有的灵活而舒适的工具。

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.