Home | All Classes | Main Classes | Annotated | Grouped Classes | Functions |
A simple answer is that when Qt was designed, it was not possible to fully exploit the template mechanism in multi-platform applications due to the inadequacies of various compilers. Even today, many widely used C++ compilers have problems with advanced templates. For example, you cannot safely rely on partial template instantiation, which is essential for some non-trivial problem domains. Thus Qt's usage of templates has to be rather conservative. Keep in mind that Qt is a multi-platform toolkit, and progress on the Linux/g++ platform does not necessarily improve the situation elsewhere.
Eventually those compilers with weak template implementations will improve. But even if all our users had access to a fully standards compliant modern C++ compiler with excellent template support, we would not abandon the string-based approach used by our meta object compiler. Here are five reasons why:
Syntax isn't just sugar: the syntax we use to express our algorithms can significantly affect the readability and maintainability of our code. The syntax used for Qt's signals and slots has proved very successful in practice. The syntax is intuitive, simple to use and easy to read. People learning Qt find the syntax helps them understand and utilize the signals and slots concept -- despite its highly abstract and generic nature. Furthermore, declaring signals in class definitions ensures that the signals are protected in the sense of protected C++ member functions. This helps programmers get their design right from the very beginning, without even having to think about design patterns.
Qt's moc (Meta Object Compiler) provides a clean way to go beyond the compiled language's facilities. It does so by generating additional C++ code which can be compiled by any standard C++ compiler. The moc reads C++ source files. If it finds one or more class declarations that contain the "Q_OBJECT" macro, it produces another C++ source file which contains the meta object code for those classes. The C++ source file generated by the moc must be compiled and linked with the implementation of the class (or it can be #included into the class's source file). Typically moc is not called manually, but automatically by the build system, so it requires no additional effort by the programmer.
There are other precompilers, for example, rpc and idl, that enable programs or objects to communicate over process or machine boundaries. The alternatives to precompilers are hacked compilers, proprietary languages or graphical programming tools with dialogs or wizards that generate obscure code. Rather than locking our customers into a proprietary C++ compiler or into a particular Integrated Development Environment, we enable them to use whatever tools they prefer. Instead of forcing programmers to add generated code into source repositories, we encourage them to add our tools to their build system: cleaner, safer and more in the spirit of UNIX.
C++ is a standarized, powerful and elaborate general-purpose language. It's the only language that is exploited on such a wide range of software projects, spanning every kind of application from entire operating systems, database servers and high end graphics applications to common desktop applications. One of the keys to C++'s success is its scalable language design that focuses on maximum performance and minimal memory consumption whilst still maintaining ANSI-C compatibility.
For all these advantages, there are some downsides. For C++, the static object model is a clear disadvantage over the dynamic messaging approach of Objective C when it comes to component-based graphical user interface programming. What's good for a high end database server or an operating system isn't necessarily the right design choice for a GUI frontend. With moc, we have turned this disadvantage into an advantage, and added the flexibility required to meet the challenge of safe and efficient graphical user interface programming.
Our approach goes far beyond anything you can do with templates. For example, we can have object properties. And we can have overloaded signals and slots, which feels natural when programming in a language where overloads are a key concept. Our signals add zero bytes to the size of a class instance, which means we can add new signals without breaking binary compatibility. Because we do not rely on excessive inlining as done with templates, we can keep the code size smaller. Adding new connections just expands to a simple function call rather than a complex template function.
Another benefit is that we can explore an object's signals and slots at runtime. We can establish connections using type-safe call-by-name, without having to know the exact types of the objects we are connecting. This is impossible with a template based solution. This kind of runtime introspection opens up new possibilities, for example GUIs that are generated and connected from Qt Designer's XML ui files.
Qt's signals and slots implementation is not as fast as a template-based solution. While emitting a signal is approximately the cost of four ordinary function calls with common template implementations, Qt requires effort comparable to about ten function calls. This is not surprising since the Qt mechanism includes a generic marshaller, introspection and ultimately scriptability. It does not rely on excessive inlining and code expansion and it provides unmatched runtime safety. Qt's iterators are safe while those of faster template-based systems are not. Even during the process of emitting a signal to several receivers, those receivers can be deleted safely without your program crashing. Without this safety, your application would eventually crash with a difficult to debug free'd memory read or write error.
Nonetheless, couldn't a template-based solution improve the performance of an application using signals and slots? While it is true that Qt adds a small overhead to the cost of calling a slot through a signal, the cost of the call is only a small proportion of the entire cost of a slot. Benchmarking against Qt's signals and slots system is typically done with empty slots. As soon as you do anything useful in your slots, for example a few simple string operations, the calling overhead becomes negligible. Qt's system is so optimized that anything that requires operator new or delete (for example, string operations or inserting/removing something from a template container) is significantly more expensive than emitting a signal.
Aside: If you have a signals and slots connection in a tight inner loop of a performance critical task and you identify this connection as the bottleneck, think about using the standard listener-interface pattern rather than signals and slots. In cases where this occurs, you probably only require a 1:1 connection anyway. For example, if you have an object that downloads data from the network, it's a perfectly sensible design to use a signal to indicate that the requested data arrived. But if you need to send out every single byte one by one to a consumer, use a listener interface rather than signals and slots.
Because we had the moc for signals and slots, we could add other useful things to it that could not not be done with templates. Among these are scoped translations via a generated tr() function, and an advanced property system with introspection and extended runtime type information. The property system alone is a great advantage: a powerful and generic user interface design tool like Qt Designer would be a lot harder to write - if not impossible - without a powerful and introspective property system.
C++ with the moc preprocessor essentially gives us the flexibility of Objective-C or of a Java Runtime Environment, while maintaining C++'s unique performance and scalability advantages. It is what makes Qt the flexible and comfortable tool we have today.
Copyright © 2003 Trolltech | Trademarks | Qt version 3.2.0b2
|