原文链接 https://doc.qt.io/archives/qt-4.8/templates.html Qt 4.8 官方文档
模板是 C++ 内建的一种机制,它允许编译器根据传入参数的类型在编译期生成代码。因此,模板对框架开发者来说极具吸引力,Qt 在很多地方也确实使用了高级模板技术。
然而,模板是有局限性的。有些东西可以很容易用模板来表达,而有些则完全不可能。例如,一个通用的向量容器类很容易用模板来实现,甚至可以针对指针类型进行偏特化;但 一个 根据字符串形式的 XML 描述来构建图形界面 的函数,是不可能用模板来表达的。
在这两者之间还存在一个灰色地带: 有些事情 理论上 可以用模板“硬写”出来,但代价是代码体积、可读性、可移植性、易用性、可扩展性、健壮性,最终还有设计的优雅性都会受到严重影响。模板和 C 预处理器都可以被扩展,实现一些极其巧妙且令人惊叹的功能,但 能做,并不意味着应该这样做。
我们必须提到一个非常现实的挑战:由于各种编译器实现不完善,在跨平台应用中,至今仍然无法充分、可靠地利用模板机制。
遗憾的是,代码不是写在书里的,而是要用真实世界中的编译器,在真实世界中的操作系统上编译的。
即便在今天,许多被广泛使用的 C++ 编译器,在高级模板方面仍然存在问题。
例如:
你不能安全地依赖 模板偏特化,而这对于某些复杂的领域至关重要
有些编译器在 模板成员函数 方面存在限制,这使得将泛型编程与面向对象编程结合变得非常困难
不过,我们并不认为这些问题是决定性的限制。
即便假设所有 Qt 用户都拥有 完全符合标准、模板支持极佳的现代 C++ 编译器,
我们仍然 不会 用基于模板的信号槽系统来取代当前基于字符串、由 moc 支持的方案。
原因如下:
语法不仅仅是“语法糖”。 我们用来表达算法的语法,会显著影响代码的可读性和可维护性。
Qt 的信号槽语法在实践中被证明是非常成功的:
直观
易用
易读
学习 Qt 的人往往会发现,这种语法能帮助他们理解并正确使用信号槽这一高度抽象、通用的概念。
此外,在类定义中声明信号可以确保信号受到保护,其保护方式类似于 C++ 的 protected 成员函数。
这能帮助程序员从一开始就形成正确的设计,而无需考虑各种设计模式。
Qt 的 moc(元对象编译器)提供了一种 超越语言本身能力的简洁方式。
moc 的工作方式是:
读取 C++ 源文件
如果发现包含 Q_OBJECT 宏的类
就生成一个额外的 C++ 源文件,其中包含该类的元对象代码
生成的 C++ 文件会被编译并与类的实现一起链接(或者直接 #include 进源文件)。
通常,moc 是由构建系统自动调用的,对程序员几乎没有额外负担。
moc 并不是 Qt 唯一使用的代码生成器。另一个著名例子是 uic(用户界面编译器):
它读取 XML 格式的 UI 描述
生成用于构建界面的 C++ 代码
在 Qt 之外,代码生成器同样非常常见:
rpc、idl:用于跨进程或跨机器通信
各种词法/语法分析器生成器,如 lex 和 yacc
相比之下,替代方案通常是:
修改过的的编译器
专有语言
或只能单向生成代码、不可维护的 GUI 向导工具
Qt 的选择是:
不把用户锁死在某个编译器或 IDE 中,而是让他们使用自己喜欢的工具,并将生成步骤集成进构建系统。
这更简洁、更安全,也更符合 UNIX 哲学。
C++ 是一种标准化、强大且复杂的通用语言,它被广泛应用于:
操作系统
数据库服务器
高端图形应用
桌面应用
C++ 成功的关键在于其可扩展的语言设计:
在保证高性能、低内存占用的同时,仍保持与 ANSI C 的兼容性。
但这些优点在 GUI 编程中也带来了缺点。
与 Objective-C 的动态消息机制相比,C++ 的 静态对象模型 在组件化 GUI 编程中是一个明显劣势。
moc 让我们把这一劣势转化为了优势,为安全、高效的 GUI 编程引入了所需的灵活性。
例如:
对象属性系统
重载的信号和槽(在支持函数重载的语言中显得非常自然)
信号不增加对象实例大小 → 可在不破坏二进制兼容性的前提下添加新信号
代码体积更小,不依赖大量内联和模板展开
更重要的是:
可以在 运行时 获取对象的信号和槽
可以通过 类型安全的名称调用 建立连接,而无需知道对象的确切类型
这些都是 模板方案无法实现的。
这种运行时内省机制开辟了新的可能性
例如可以根据 Qt Designer 的 XML UI 文件生成界面和信号槽。
Qt 的信号槽确实比模板方案慢一些:
模板实现:大约相当于 4 次普通函数调用
Qt:大约相当于 10 次函数调用
这是正常的,因为 Qt 的机制还包含:
通用参数封送(marshaller)
运行时自省
跨线程的队列调用
脚本化支持
Qt 不依赖于过多的内联和代码扩展,提供了 无与伦比的运行时安全性。
例如:
在信号发射过程中,槽对象可以被安全地删除
不会出现难以调试的悬空指针或非法内存访问
很多性能测试都是对“空槽”进行的。一旦槽中执行了任何有意义的工作(例如一些简单的字符串操作),信号调用的开销就变得可以忽略不计了。
事实上:
任何涉及 new / delete 的操作
或模板容器的插入/删除
都比发射一个 Qt 信号开销大。
备注: 如果你真的在性能关键的内层循环中(比如每个字节/每个样本/每次迭代都要触发)发现信号槽成为瓶颈,说明你可能需要的是 监听器接口(listener interface)模式,而不是信号槽。例如,如果您有一个从网络下载数据的对象,使用信号来指示请求的数据已到达是完全合理的。但是,如果您需要将每个字节逐个发送给消费者,则应使用监听器接口模式,而不是信号槽。
译者注:高频数据推送(如 1ms 一次或更细粒度)不宜使用信号槽,改用一对一回调或监听器接口,并尽量采用按块传输的方式。
正是因为有 moc,我们才能加入 模板完全做不到的功能,例如:
tr() 函数实现翻译
强大的属性系统(支持自省和扩展运行时类型信息)
不依赖 RTTI 的 qobject_cast<T>(),我们使用它来安全地查询动态加载组件的接口。
动态元对象(如 ActiveX 组件的动态封装和导出)
这些能力是 Qt Designer、插件系统和组件互操作的基础,而 模板无法提供这些能力。
通过 moc,Qt 在 C++ 之上构建了一套机制,使其在灵活性上接近 Objective-C 或 Java 运行时,同时仍然保持了 C++ 在性能和可扩展性上的独特优势。
这正是 Qt 成为今天这样一个灵活、强大、易用工具的原因。