Why Does Qt Use Moc for Signals and Slots?
模板是 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
, 我们将这一劣势转化为优势, 并增加了应对安全高效的图形用户界面编程挑战所需的灵活性.
我们的方法远远超出了你使用模板所能做的任何事情. 例如, 我们可以拥有对象属性. 我们可以拥有重载的信号和槽函数, 这在使用重载是关键概念的语言进行编程时感觉很自然. 我们的信号向类实例的大小添加零字节, 这意味着我们可以在不破坏二进制兼容性的情况下添加新信号.
另一个好处是我们可以在运行时探索对象的信号和槽. 我们可以使用类型安全的名称调用来建立连接, 而不必知道我们正在连接的对象的确切类型. 对于基于模板的解决方案来说这是不可能的. 这种运行时自省开辟了新的可能性, 例如从 Qt Designer 的 XML UI 文件生成和连接的 GUI.
调用性能并不代表一切
Qt 的信号和槽实现不如基于模板的解决方案快. 虽然发出一个信号的成本大约是四个具有通用模板实现的普通函数调用的成本, 但 Qt 所需的工作量相当于大约十个函数调用. 这并不奇怪, 因为 Qt 机制包括通用编组器, 内省, 不同线程之间的排队调用以及最终的脚本能力. 它不依赖于过多的内联和代码扩展, 并且提供了无与伦比的运行时安全性. Qt 的迭代器是安全的, 而更快的基于模板的系统的迭代器则不然. 即使在向多个接收器发出信号的过程中, 也可以安全地删除这些接收器, 而不会导致程序崩溃. 如果没有这种安全性, 你的应用程序最终会崩溃, 并出现难以调试的释放内存读取或写入错误.
尽管如此, 基于模板的解决方案难道不能提高使用信号和槽的应用程序的性能吗? 虽然 Qt 确实为通过信号调用槽函数的成本增加了少量开销, 但调用的成本仅占整个成本的一小部分. 针对 Qt 信号和槽系统的基准测试通常是使用空槽来完成的. 一旦你在槽中执行任何有用的操作 (例如一些简单的字符串操作), 调用开销就可以忽略不计. Qt 的系统经过如此优化, 以至于任何需要运算符 new 或 delete 的操作 (例如, 字符串操作或从模板容器中插入/删除某些内容) 都比发出信号要昂贵得多.
旁白: 如果性能关键任务的紧密内部循环中有信号和槽连接, 并且你将该连接确定为瓶颈, 请考虑使用标准监听器接口模式而不是信号和槽. 如果发生这种情况, 你可能只需要 1:1 连接. 例如, 如果你有一个从网络下载数据的对象, 那么使用信号来指示请求的数据已到达是一个非常明智的设计. 但是, 如果你需要将每个字节一一发送给消费者, 请使用监听器接口而不是信号和槽.
无限制
因为我们有信号和槽的 moc
, 所以我们可以向其中添加其他用模板无法完成的有用的东西. 其中包括通过生成的 tr()
函数进行范围转换, 以及具有内省和扩展运行时类型信息的高级属性系统. 属性系统本身就是一个巨大的优势: 如果没有强大且内省的属性系统, 像 Qt Designer 这样强大且通用的用户界面设计工具将很难编写 (如果不是不可能的话). 但事情并没有就此结束. 我们还提供了动态 qobject_cast<T>() 机制, 该机制不依赖于系统的 RTTI, 因此不具有其局限性. 我们使用它来安全地从动态加载的组件中查询接口. 另一个应用领域是动态元对象. 例如, 我们可以使用 ActiveX 组件并在运行时围绕它创建一个元对象. 或者我们可以通过导出其元对象将 Qt 组件导出为 ActiveX 组件. 你无法使用模板执行这些操作.
带有 moc
的 C++ 实质上为我们提供了 Objective-C 或 Java 运行时环境的灵活性, 同时保持了 C++ 独特的性能和可扩展性优势. 这使得 Qt 成为我们今天拥有的灵活而舒适的工具.