本页内容

Qt 为何在信号与槽中使用 Moc?

模板是 C++ 中的内置机制,它允许编译器根据传递的参数类型动态生成代码。因此,模板对框架开发者极具吸引力,我们在 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使用的唯一代码生成器。另一个突出的例子是uic (User Interface Compiler )。它接受XML格式的用户界面描述,并生成用于设置表单的C++代码。在Qt之外,代码生成器同样很常见。 例如rpc 和idl ,它们使程序或对象能够跨越进程或机器边界进行通信。还有种类繁多的扫描器和解析器生成器,其中lex 和yacc 最为知名。它们以语法规范为输入,生成实现状态机的代码。 代码生成器的替代方案包括“临时拼凑”的编译器、专有语言,或是带有单向对话框或向导的图形化编程工具——这些工具在设计时(而非编译时)生成晦涩难懂的代码。我们不将客户锁定在专有的 C++ 编译器或特定的集成开发环境中,而是让他们能够自由使用自己偏好的任何工具。 我们不强迫程序员将生成的代码添加到源代码仓库中,而是鼓励他们将我们的工具集成到其构建系统中:这样更简洁、更安全,也更符合 UNIX 精神。

GUI 是动态的

C++ 是一种标准化、强大且结构精巧的通用编程语言。它是唯一被广泛应用于如此广泛软件项目的语言,涵盖从整个操作系统、数据库服务器和高端图形应用程序到常见桌面应用程序等各类应用。 C++ 成功的关键之一在于其可扩展的语言设计,该设计在保持 ANSI C 兼容性的同时,致力于实现最高性能和最低内存消耗。

尽管具备这些优势,但也存在一些缺点。对于 C++ 而言,在基于组件的图形用户界面编程方面,其静态对象模型相对于 Objective C 的动态消息传递方法显然处于劣势。 对高端数据库服务器或操作系统而言的优点,未必是图形用户界面前端(GUI)的正确设计选择。借助moc ,我们将这一劣势转化为优势,并增添了应对安全、高效图形用户界面编程挑战所需的灵活性。

我们的方法远超模板所能实现的范围。例如,我们可以定义对象属性;还可以重载信号和槽,这在以重载为核心概念的编程语言中显得十分自然。我们的信号不会增加类实例的大小,这意味着我们可以添加新信号而不会破坏二进制兼容性。

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

调用性能并非一切

Qt 的信号与槽实现速度不如基于模板的解决方案。在常见的模板实现中,发出一个信号的开销大约相当于四个普通函数调用,而 Qt 所需的开销则相当于大约十个函数调用。 这并不令人意外,因为 Qt 的机制包含通用序列化器、内省、不同线程之间的队列调用,以及最终的可脚本化特性。它不依赖于过度的内联和代码展开,并提供了无与伦比的运行时安全性。 Qt 的迭代器是安全的,而速度更快的基于模板的系统中的迭代器则不然。即使在向多个接收者发出信号的过程中,这些接收者也可以被安全地删除,而不会导致程序崩溃。如果没有这种安全性,您的应用程序最终会因难以调试的已释放内存读写错误而崩溃。

尽管如此,基于模板的解决方案难道不能提高使用信号与槽的应用程序的性能吗?虽然 Qt 确实给通过信号调用槽带来了些许开销,但该调用的开销仅占槽总开销的一小部分。 针对 Qt 信号与槽系统的性能测试通常使用空槽进行。一旦你在槽中执行任何有用的操作(例如一些简单的字符串操作),调用开销便微不足道了。 Qt 的系统经过高度优化,任何需要使用 `operator new` 或 `operator delete` 的操作(例如字符串操作,或向模板容器中插入/移除元素),其开销都远高于发出一个信号。

附注:如果性能关键任务的紧凑内部循环中存在信号与槽连接,且你已确认该连接是瓶颈,请考虑使用标准的监听器-接口模式,而非信号与槽。在这种情况下,你可能本来就只需要一对一的连接。 例如,如果你有一个从网络下载数据的对象,使用信号来指示请求的数据已到达,这是一种非常合理的设计。但如果你需要将每个字节逐一发送给消费者,请使用监听器接口而非信号与槽。

无限制

由于我们拥有用于信号和插槽的moc ,因此能够向其中添加一些仅靠模板无法实现的实用功能。其中包括通过生成的tr() 函数实现的作用域转换,以及具备内省功能和扩展运行时类型信息的先进属性系统。 仅属性系统本身就具有巨大优势:如果没有强大且支持自省的属性系统,像Qt Widgets Designer 这样功能强大且通用的用户界面设计工具将难以编写——甚至根本无法实现。但这还不是全部。 我们还提供了一种动态的qobject_cast<T>() 机制,它不依赖于系统的 RTTI,因此也不受其限制。 我们利用它从动态加载的组件中安全地查询接口。另一个应用领域是动态元对象。例如,我们可以对 ActiveX 组件在运行时创建一个元对象。或者,我们可以通过导出其元对象,将 Qt 组件作为 ActiveX 组件导出。这些操作都无法通过模板实现。

结合moc 的C++,本质上为我们提供了与Objective-C或Java运行时环境相当的灵活性,同时保留了C++独特的性能和可扩展性优势。正是这一点,使Qt成为了我们今天所拥有的灵活且易用的工具。

© 2026 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.