Warum verwendet Qt Moc für Signale und Slots?

Templates sind ein eingebauter Mechanismus in C++, der es dem Compiler ermöglicht, Code in Abhängigkeit vom Typ der übergebenen Argumente zu generieren. Als solche sind Templates für Framework-Entwickler hochinteressant, und wir verwenden erweiterte Templates an vielen Stellen in Qt. Es gibt jedoch Einschränkungen: Es gibt Dinge, die man leicht mit Templates ausdrücken kann, und es gibt Dinge, die man unmöglich mit Templates ausdrücken kann. Eine generische Vektor-Containerklasse lässt sich leicht ausdrücken, sogar mit partieller Spezialisierung für Zeigertypen, während eine Funktion, die eine grafische Benutzeroberfläche auf der Grundlage einer XML-Beschreibung in Form eines Strings einrichtet, nicht als Vorlage ausgedrückt werden kann. Und dann gibt es noch eine Grauzone dazwischen. Dinge, die man mit Schablonen auf Kosten der Codegröße, der Lesbarkeit, der Portabilität, der Benutzerfreundlichkeit, der Erweiterbarkeit, der Robustheit und letztlich der Schönheit des Designs hacken kann. Sowohl mit Templates als auch mit dem C-Präprozessor lassen sich unglaublich intelligente und verblüffende Dinge anstellen. Aber nur weil diese Dinge möglich sind, heißt das nicht zwangsläufig, dass es die richtige Entscheidung ist, sie zu tun. Code ist leider nicht dafür gedacht, in Büchern veröffentlicht zu werden, sondern mit realen Compilern auf realen Betriebssystemen kompiliert zu werden.

Hier sind einige Gründe, warum Qt das moc verwendet:

Syntax ist wichtig

Syntax ist nicht nur Zucker: Die Syntax, die wir verwenden, um unsere Algorithmen auszudrücken, kann die Lesbarkeit und Wartbarkeit unseres Codes erheblich beeinflussen. Die Syntax, die für die Signale und Slots von Qt verwendet wird, hat sich in der Praxis sehr bewährt. Die Syntax ist intuitiv, einfach zu verwenden und leicht zu lesen. Menschen, die Qt lernen, finden, dass die Syntax ihnen hilft, das Konzept der Signale und Slots zu verstehen und zu nutzen - trotz seiner sehr abstrakten und generischen Natur. Dies hilft Programmierern, ihr Design von Anfang an richtig zu gestalten, ohne überhaupt über Design Patterns nachdenken zu müssen.

Code-Generatoren sind gut

Der moc (Meta Object Compiler) von Qt bietet eine saubere Möglichkeit, über die Möglichkeiten der kompilierten Sprache hinauszugehen. Dies geschieht durch die Generierung von zusätzlichem C++-Code, der von jedem Standard-C++-Compiler kompiliert werden kann. Der moc liest C++-Quelldateien. Wenn es eine oder mehrere Klassendeklarationen findet, die das Makro Q_OBJECT enthalten, erzeugt es eine weitere C++-Quelldatei, die den Meta-Objektcode für diese Klassen enthält. Die von moc erzeugte C++-Quelldatei muss kompiliert und mit der Implementierung der Klasse gelinkt werden (oder sie kann unter #included in die Quelldatei der Klasse eingebunden werden). Normalerweise wird moc nicht manuell aufgerufen, sondern automatisch vom Build-System, so dass es keinen zusätzlichen Aufwand für den Programmierer erfordert.

Der moc ist nicht der einzige Codegenerator, den Qt verwendet. Ein weiteres prominentes Beispiel ist uic (User Interface Compiler). Er nimmt eine Beschreibung der Benutzeroberfläche in XML und erstellt C++-Code, der das Formular einrichtet. Außerhalb von Qt sind Code-Generatoren ebenfalls weit verbreitet. Nehmen Sie zum Beispiel rpc und idl, die es Programmen oder Objekten ermöglichen, über Prozess- oder Maschinengrenzen hinweg zu kommunizieren. Oder die große Vielfalt von Scanner- und Parser-Generatoren, von denen lex und yacc die bekanntesten sind. Sie nehmen eine Grammatikspezifikation als Eingabe und erzeugen Code, der einen Zustandsautomaten implementiert. Die Alternativen zu Codegeneratoren sind gehackte Compiler, proprietäre Sprachen oder grafische Programmierwerkzeuge mit einseitigen Dialogen oder Assistenten, die obskuren Code während der Entwurfszeit und nicht während der Kompilierungszeit erzeugen. Anstatt unsere Kunden an einen proprietären C++-Compiler oder eine bestimmte integrierte Entwicklungsumgebung zu binden, ermöglichen wir ihnen die Verwendung der von ihnen bevorzugten Tools. Anstatt Programmierer zu zwingen, generierten Code in Quellcode-Repositories einzubinden, ermutigen wir sie, unsere Tools in ihr Build-System einzubinden: sauberer, sicherer und mehr im Sinne von UNIX.

GUIs sind dynamisch

C++ ist eine standardisierte, leistungsstarke und ausgefeilte Allzwecksprache. Sie ist die einzige Sprache, die in einem so breiten Spektrum von Softwareprojekten eingesetzt wird, das alle Arten von Anwendungen umfasst, von ganzen Betriebssystemen, Datenbankservern und High-End-Grafikanwendungen bis hin zu einfachen Desktop-Anwendungen. Einer der Schlüssel zum Erfolg von C++ ist sein skalierbares Sprachdesign, das sich auf maximale Leistung und minimalen Speicherverbrauch konzentriert und gleichzeitig die Kompatibilität mit ANSI C beibehält.

Bei all diesen Vorteilen gibt es auch einige Schattenseiten. Für C++ ist das statische Objektmodell ein klarer Nachteil gegenüber dem dynamischen Messaging-Ansatz von Objective C, wenn es um die komponentenbasierte Programmierung von grafischen Benutzeroberflächen geht. Was für einen High-End-Datenbankserver oder ein Betriebssystem gut ist, ist nicht unbedingt die richtige Designwahl für ein GUI-Frontend. Mit moc haben wir diesen Nachteil in einen Vorteil verwandelt und die Flexibilität hinzugefügt, die erforderlich ist, um die Herausforderung der sicheren und effizienten Programmierung von grafischen Benutzeroberflächen zu meistern.

Unser Ansatz geht weit über alles hinaus, was man mit Vorlagen machen kann. Wir können zum Beispiel Objekteigenschaften haben. Und wir können überladene Signale und Slots verwenden, was sich bei der Programmierung in einer Sprache, in der Überladungen ein Schlüsselkonzept sind, ganz natürlich anfühlt. Unsere Signale fügen null Bytes zur Größe einer Klasseninstanz hinzu, was bedeutet, dass wir neue Signale hinzufügen können, ohne die Binärkompatibilität zu verletzen.

Ein weiterer Vorteil ist, dass wir die Signale und Slots eines Objekts zur Laufzeit untersuchen können. Wir können Verbindungen mit typsicheren Call-by-Name-Verfahren herstellen, ohne die genauen Typen der zu verbindenden Objekte kennen zu müssen. Dies ist bei einer vorlagenbasierten Lösung nicht möglich. Diese Art der Introspektion zur Laufzeit eröffnet neue Möglichkeiten, zum Beispiel GUIs, die aus den XML-UI-Dateien von Qt Widgets Designer generiert und verbunden werden.

Aufrufleistung ist nicht alles

Die Signal- und Slot-Implementierung von Qt ist nicht so schnell wie eine Template-basierte Lösung. Während das Aussenden eines Signals bei gängigen Template-Implementierungen ungefähr den Aufwand von vier gewöhnlichen Funktionsaufrufen bedeutet, erfordert Qt einen Aufwand, der mit etwa zehn Funktionsaufrufen vergleichbar ist. Dies ist nicht überraschend, da der Qt-Mechanismus einen generischen Marshaller, Introspektion, Warteschlangenaufrufe zwischen verschiedenen Threads und schließlich Skriptfähigkeit umfasst. Er ist nicht auf übermäßiges Inlining und Code-Expansion angewiesen und bietet eine unübertroffene Laufzeitsicherheit. Die Iteratoren von Qt sind sicher, während die von schnelleren Template-basierten Systemen dies nicht sind. Selbst während der Aussendung eines Signals an mehrere Empfänger können diese Empfänger sicher gelöscht werden, ohne dass Ihr Programm abstürzt. Ohne diese Sicherheit würde Ihre Anwendung schließlich mit einem schwer zu debuggenden Fehler beim Lesen oder Schreiben von freiem Speicher abstürzen.

Könnte eine auf Vorlagen basierende Lösung nicht trotzdem die Leistung einer Anwendung verbessern, die Signale und Slots verwendet? Es stimmt zwar, dass Qt einen kleinen Overhead zu den Kosten des Aufrufs eines Slots durch ein Signal hinzufügt, aber die Kosten des Aufrufs machen nur einen kleinen Teil der Gesamtkosten eines Slots aus. Ein Benchmarking mit dem Signal- und Slot-System von Qt wird normalerweise mit leeren Slots durchgeführt. Sobald Sie etwas Nützliches in Ihren Slots tun, zum Beispiel ein paar einfache String-Operationen, wird der Aufruf-Overhead vernachlässigbar. Das System von Qt ist so optimiert, dass alles, was den Operator new oder delete erfordert (z.B. String-Operationen oder das Einfügen/Entfernen von etwas aus einem Template-Container), deutlich teurer ist als das Aussenden eines Signals.

Nebenbei: Wenn Sie eine Signal- und Slot-Verbindung in einer engen inneren Schleife einer leistungskritischen Aufgabe haben und Sie diese Verbindung als Engpass identifizieren, sollten Sie darüber nachdenken, das Standard-Listener-Interface-Muster statt Signale und Slots zu verwenden. In den Fällen, in denen dies der Fall ist, benötigen Sie wahrscheinlich ohnehin nur eine 1:1-Verbindung. Wenn Sie zum Beispiel ein Objekt haben, das Daten aus dem Netz herunterlädt, ist es durchaus sinnvoll, ein Signal zu verwenden, um anzuzeigen, dass die angeforderten Daten angekommen sind. Wenn Sie jedoch jedes einzelne Byte an einen Verbraucher senden müssen, sollten Sie eine Hörerschnittstelle verwenden, anstatt Signale und Slots.

Keine Begrenzungen

Da wir die moc für Signale und Slots hatten, konnten wir andere nützliche Dinge hinzufügen, die mit Vorlagen nicht möglich waren. Dazu gehören skalierte Übersetzungen über eine generierte tr() Funktion und ein erweitertes Eigenschaftssystem mit Introspektion und erweiterten Laufzeit-Typinformationen. Das Eigenschaftssystem allein ist schon ein großer Vorteil: ein leistungsfähiges und generisches Werkzeug zur Gestaltung von Benutzeroberflächen wie Qt Widgets Designer wäre ohne ein leistungsfähiges und introspektives Eigenschaftssystem sehr viel schwieriger zu schreiben - wenn nicht gar unmöglich -. Aber das ist noch nicht alles. Wir stellen auch einen dynamischen qobject_cast<T>()-Mechanismus zur Verfügung, der sich nicht auf die RTTI des Systems stützt und daher auch nicht deren Einschränkungen unterliegt. Wir verwenden ihn, um Schnittstellen von dynamisch geladenen Komponenten sicher abzufragen. Eine weitere Anwendungsdomäne sind dynamische Meta-Objekte. Wir können z.B. ActiveX-Komponenten nehmen und zur Laufzeit ein Metaobjekt um sie herum erzeugen. Oder wir können Qt-Komponenten als ActiveX-Komponenten exportieren, indem wir ihr Meta-Objekt exportieren. Beides kann man mit Templates nicht machen.

C++ mit moc gibt uns im Wesentlichen die Flexibilität von Objective-C oder einer Java-Laufzeitumgebung, während die einzigartigen Leistungs- und Skalierbarkeitsvorteile von C++ erhalten bleiben. Das ist es, was Qt zu dem flexiblen und komfortablen Werkzeug macht, das wir heute haben.

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