by Volker Hilsheimer |
The moc tool and QObject add flexibility and power to the C++ language. They add, among other things, the signals and slots paradigm and QObject's memory management mechanism. Both are very powerful and sometimes we wish we could use them in template classes and in conjunction with multiple inheritance.
In this article, we will look closer at why moc does not support these use cases and how to work around these restrictions in practice.
Connecting to Templates |
The meta-object compiler, moc, requires a class's API to be completely defined by its definition; moc can then parse the definition and create a "meta-object" that stores information about all the properties, signals, and slots of that class.
A template class is not defined only by definition but also by instantiation. The template class itself is just a blueprint that the C++ compiler uses to generate the actual C++ classes later on. There can be an arbitrary number of instantiations with different template parameters, and each of these instantiations would require a different meta-object.
While it is theoretically possible for moc to handle templates, it would be extremely complex to implement, and would be highly impractical to use: For each template instantiation, moc would have to generate the appropriate meta-object code, and the generated code would have to be included once per link unit---which becomes a nightmare to maintain once a template class is used with the same template parameter in different compilation units.
Therefore, running moc on a file declaring a template class like the one below prints a friendly error message ("Sorry, Qt does not support templates that contain signals, slots or Q_OBJECT"):
// WON'T WORK templateIf the signals and slots don't require the template parameter to be part of the prototype, the workaround is to make a template class inherit a QObject subclass that provides the required signals and slots:
class AQObjectSubclass : public QObject { Q_OBJECT public: AQObjectSubclass(QObject *parent = 0); public slots: void setValue(const QString &value); signals: void valueChanged(const QString &value); ... }; template <typename T> class ATemplateClass : public AQObjectSubclass { public: ATemplateClass(QObject *parent = 0); void someOtherFunction(const T &value); ... };
If signals and slots need to use the template parameters, the Observer pattern is an alternative.
The Observer Pattern |
The Observer pattern is a mechanism for notifying changes of state in one object to one or more other objects. In Qt programming, it is seldom used, because signals and slots fulfill that role very well.
Instead of emitting signals, an observable class calls virtual functions on a list of "observers" (or "listeners"). The observers are objects that implement a particular interface and respond to it. For example:
template <typename T> class AnObserver { public: void valueChanged(const T &value) = 0; }; template <typename T> class ATemplateClass { public: void installObserver(AnObserver *observer) { myObservers.append(observer); } void removeObserver(AnObserver *observer) { myObservers.removeAll(observer); } void setValue(const T &value) { if (myValue != value) { myValue = value; foreach (AnObserver *observer, myObservers) observer->valueChanged(myValue); } } private: T myValue; QList<AnObserver *> myObservers; };
Compared to signals and slots, the Observer pattern is quite heavyweight: It requires us to manually keep a list of observers, to provide functions for installing and removing observers, and to iterate through that list whenever we want to notify the observers (the equivalent of emitting a signal). It also requires us to subclass the observer class whenever we want to be notified of a change in the observable class (the "subject" class).
Although Qt is mostly signal--slot based, it uses the Observer pattern for event filters. Event filters are installed by calling QObject::installEventFilter(); afterwards, events are delivered to the observers through the QObject::eventFilter() virtual function. In this case, the observer class and the subject class both inherit the same class (QObject).
Multiple Inheritance |
Inheriting from multiple QObject superclasses not only makes the meta-object difficult to generate, it also complicates the concept of object ownership. Indeed, inheriting multiple times from QObject will generate a compile error when using the Q_OBJECT macro, or later on when compiling the meta-object code. All those errors point at ambiguities in how to cast to QObject or how to use QObject's API.
The most common usage pattern for using multiple inheritance in C++ is for implementing interfaces. Interfaces are abstract classes, where each function is declared as pure virtual. These functions are then implementated in a separate class that is usually a subclass of one or more interface classes. This pattern enforces a strict separation of a well-defined public API from the internal implementation details. Qt 4's plugin system is based on interfaces.
For example:
class ClipboardInterface { public: virtual void cut() = 0; virtual void copy() const = 0; virtual void paste() = 0; };
The class implementing the above interface can be a QObject subclass, and even implement the pure virtual functions as slots:
class CustomWidget : public QWidget, public ClipboardInterface { Q_OBJECT public: CustomWidget(QWidget *parent = 0); public slots: void cut(); void copy() const; void paste(); };
However, it would be nicer if the interface class itself were able to declare the functions as slots---this would make implementing the interface less error-prone and would also allow the interface to support introspection.
But making the interface class a QObject introduces the problem of multiple inheritance from QObject as soon as the interface is implemented by a QObject, as would be the situation in the CustomWidget case.
To avoid the multiple inheritance, we can use a "has a" relationship:
class CustomWidget : public QWidget, public ClipboardInterface { Q_OBJECT public: CustomWidget(QWidget *parent = 0); void cut(); void copy() const; void paste(); private: ClipboardWrapper *wrapper; };
The CustomWidget class "has a" ClipboardWrapper object. Here's the ClipboardWrapper class definition:
class ClipboardWrapper : public QObject, public ClipboardInterface { Q_OBJECT public: ClipboardWrapper(QObject *parent) : QObject(parent) { Q_ASSERT(parent); wrappedObject = qobject_cast<ClipboardInterface *>(parent); Q_ASSERT(wrappedObject); } public slots: void cut() { wrappedObject->cut(); } void copy() const { wrappedObject->copy(); } void paste() { wrappedObject->paste(); } private: ClipboardInterface *wrappedObject; };
Code that would usually connect to the CustomWidget object can instead connect to the CustomWidget's wrapper object. The wrapper object can communicate with the wrapped object through a well-defined interface, and changes to the interfaces will trigger compile-time errors that help the software developer implement the interfaces correctly in the different places it is used.
Adding signals follows a similar pattern:
class ClipboardEvents { public: virtual void copyAvailableChange(bool available) = 0; virtual void pasteAvailableChange(bool available) = 0; }; class ClipboardWrapper : public QObject, public ClipboardInterface§, public ClipboardEvents { Q_OBJECT public: ClipboardWrapper( QObject *parent); public slots: void cut() { wrappedObject->cut(); } void copy() const { wrappedObject->copy(); } void paste() { wrappedObject->paste(); } signals: void copyAvailableChange(bool available); void pasteAvailableChange(bool available); private: ClipboardInterface *wrappedObject; };
Since the CustomWidget class must emit the signals from its own code, and since signals are protected functions, we must use a cast to call the signals through the interface declaration (which declared the functions as public):
void CustomWidget::paste() { bool wasAvailable = isCopyAvailable(); ... if (wasAvailable != isCopyAvailable()) emit static_cast<ClipboardEvents *>(wrapper)-> copyAvailableChange(isCopyAvailable()); }
At first sight, this pattern might appear to be over-engineered, but it scales very well as soon as many classes use the same interface/wrapper pair, and as soon as application code can use identical APIs for many different objects.
Copyright © 2005 Trolltech | Trademarks |