Quelques mythes sur le générateur de métaobjets de Qt brisés
Son mainteneur défend l'outil

Le , par dourouc05, Responsable Qt & Livres
Qt est un framework C++ complet, parti des interfaces graphiques dans les années 1990 (avant la normalisation de C++) et maintenant extrêmement généraliste. L’un de ses points forts est probablement la connexion entre signaux et slots pour écrire des applications interactives : lorsqu’il se passe quelque chose dans l’application (l’utilisateur appuie sur un bouton, un paquet arrive du réseau, etc.), un signal est émis ; ensuite, le programmeur peut connecter un slot à ce signal : ce bout de code sera exécuté chaque fois que le signal sera émis. Pour implémenter cette fonctionnalité, dans les années 1990, vu l’état disparate des compilateurs, utiliser les templates C++ était impossible : les développeurs de Qt ont alors utilisé un générateur de code, nommé moc (metaobject compiler), qui sert d’autres objectifs (l’introspection et la réflexion, notamment). Avec les années, le système a été conservé, malgré le bon nombre de critiques : son importance a encore un peu grandi lors de l’arrivée de Qt Quick. Olivier Goffart, actuel mainteneur de l’outil, tente de remettre les pendules à l’heure.

Quelques mythes

moc ne réécrit pas le code
qui lui est passé, il ne le réécrit pas : il l’analyse et génère de nouveaux fichiers C++, qui sont ensuite compilés de manière totalement indépendante. Sans l’outil, pour utiliser l’architecture actuelle de Qt, il serait nécessaire d’écrire de grandes quantités de code pour les tables d’introspection et d’autres détails nécessaires pour le fonctionnement des signaux et slots.

Il se concentre sur une série de macros définies par Qt pour générer son code : Qt ne définit pas de « nouveaux mots clés » C++. Pour définir une classe héritant de QObject, la macro Q_OBJECT évite au programmeur d’écrire une série de déclarations de fonctions (dont le code est généré par moc). Les signaux sont définis dans un bloc signals:, qui n’est autre qu’une macro qui correspond à public:. Bon nombre d’autres macros utilisées par le moc ne génèrent aucun code. Globalement, le code Qt reste du code C++, ce qui fait que tous les outils C++ traditionnels restent utilisables.

Son utilisation ne complique pas la compilation ou le débogage : la plupart des systèmes de compilation traitent de manière native moc  ; de toute façon, il s’agit simplement d’une commande supplémentaire à appliquer sur les fichiers d’en-tête. En cas d’erreur à la compilation (ce qui est très rare), le code généré est plus facile à comprendre que l’embrouillamini généré par les templates C++.

Certains ont tenté de supprimer moc, comme CopperSpice, en promettant notamment une amélioration de performance. Cependant, cette affirmation n’est pas sous-tendue par des chiffres : le graphique ci-dessous indique que la taille des exécutables générés est bien plus grande avec CopperSpice (sans moc) que Qt (4 ou 5, avec moc). De manière générale, le code généré par moc est très efficace : il est entièrement statique et évite toute allocation dynamique de mémoire, ses données sont stockées dans les segments de données en lecture seule — là où CopperSpice génère ces mêmes données à l’exécution.



Finalement, moc évolue : depuis Qt 5, il gère complètement les macros, ce qui permet de les utiliser pour définir des signaux, des slots, des classes de base, etc. Q_PROPERTY n’autorisait pas, jusqu’il y a peu, les virgules dans ses arguments (par exemple, en lui passant un QMap), ce qui a été réglé en un rien de temps.

Des fonctionnalités absentes

Finalement, la plus grande critique de moc est probablement qu’il ne gère pas les classes avec des templates, des classes imbriquées ou l’héritage multiple. Cependant, si elles ne sont pas implémentées, c’est principalement parce qu’elles ne sont pas jugées importantes. Par exemple, une implémentation des templates a été rejetée il y a trois ans ; moc-ng, une implémentation de moc sous la forme d’extension du compilateur Clang, gère sans problème les templates et les classes imbriquées. L’héritage multiple reste possible à condition que QObject soit la première classe de base — ce qui permet bon nombre d’optimisations, de telle sorte que qobject_cast est bien plus rapide que la version standard dynamic_cast.

Et donc ?

moc n’est pas vraiment un problème, ses plus grands pourfendeurs sont généralement ceux qui le connaissent le moins. L’API et notamment le système de métaobjets et la connexion entre signaux et slots sont à la base du succès de Qt. Les avantages des autres solutions sont loin d’être clairs et moc n’est pas une limitation pour les développeurs.

Source : Moc myths debunked.
Ce contenu a été publié dans Qt par dourouc05.


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster une réponse Signaler un problème

Avatar de Aurelien.Regat-Barrel Aurelien.Regat-Barrel - Expert éminent https://www.developpez.com
le 05/04/2016 à 16:12
Citation Envoyé par dourouc05 Voir le message
De manière générale, le code généré par moc est très efficace : il est entièrement statique
Ce qui était aussi (surtout?) reproché aux signaux/slots de Qt était l'impossibilité de vérifier les connections à la compilation mais au contraire d'avoir une résolution systématiquement dynamique. C'était loin d'être safe et efficace, par rapport à ce que Boost.Signal offrait depuis pas mal d'années déjà.

Qt5 a changé (corrigé) cela, et continue d'offrir ses services caractéristiques bien pratiques : connections asynchrone (entre threads), cleanup automatique des slots connectés quand l'objet cible est détruit... On peut même maintenant connecter une classe qui n'hérite pas de QObject grâce aux lambdas.

Le trop du top serait de pouvoir déclarer ses signaux à la Boost.Signal au lieu de devoir hériter de QObject. Car moc a tout de même un inconvénient de taille : il double le nombre de fichiers .cpp à compiler, et donc augmente de façon significative le temps de build, à moins d'utiliser certaines astuces (include les .moc dans ses .cpp...). Et moc lui même n'est pas super rapide : sur des gros headers (legacy code), il prend une plombe à s'exécuter, qui plus est de façon non parallèle sur les project VC++

Par rapport à:
Citation Envoyé par dourouc05 Voir le message
qobject_cast est bien plus rapide que la version standard dynamic_cast
J'aurais bien aimé avoir un benchmark / une justification... Je l'ai trouvée dans les commentaires de l'article d'origine:
dynamic_cast has more work to do because it need to check for virtual inheritance or multiple inheritance.
qobject_cast can just go up quickly in the QMetaObject hierarchy and do a simple conversion.
Avatar de Markand Markand - Membre averti https://www.developpez.com
le 05/04/2016 à 16:28
De toute façon CopperSpice c'est mort né. Ça se base sur Qt4 et ça utilise des les outils obsolète autoconf.
Avatar de LittleWhite LittleWhite - Responsable 2D/3D/Jeux https://www.developpez.com
le 05/04/2016 à 19:53
Bonjour,

Pour le moc sur les projets VC++, jom.exe ne règle t-il pas les soucis ?
C'est ce qu'utilise QtCreator/QMake et c'est normalement QMake qui fait les fichiers des projets VS, donc cela devrait passer, non ?
Avatar de chrtophe chrtophe - Responsable Systèmes https://www.developpez.com
le 05/04/2016 à 21:18
N'étant pas développeur de métier, je n'ai pas à me préoccuper de moc, c'est la compilation qui le fait pour moi. Je peux en respectant quelques règles simples, juste à ajouter Q_OBJECT sans avoir besoin de comprendre les mécanismes.

Si moc était remplacé par un autre système, je ne m'en apercevrais même pas. Donc pour moimoc n'est pas un problème.

Mais je n'ai pas le niveau pour vraiment juger si ça peut être un problème dans des cas précis, mis à part le problème évoqué par Auréilien de vérification à la connexion. Je n'ai pas migré sur Qt5 (je n'ai fait que de tous petits projets amateurs).
Avatar de foetus foetus - Expert confirmé https://www.developpez.com
le 05/04/2016 à 21:52
Citation Envoyé par chrtophe Voir le message
Mais je n'ai pas le niveau pour vraiment juger si ça peut être un problème dans des cas précis, mis à part le problème évoqué par Auréilien de vérification à la connexion. Je n'ai pas migré sur Qt5 (je n'ai fait que de tous petits projets amateurs).
Le problème est surtout que moc c'est de la "précompilation"

Et tant que tu as un outil qui configure ton projet à ta place c'est bon. Mais à la main c'est l'enfer
Avatar de dourouc05 dourouc05 - Responsable Qt & Livres https://www.developpez.com
le 06/04/2016 à 9:08
Citation Envoyé par LittleWhite Voir le message
Pour le moc sur les projets VC++, jom.exe ne règle t-il pas les soucis ?
Non : jom est une version parallélisée de nmake ; en d'autres termes, un équivalent de make, pas de qmake. Avec VS, le projet généré par qmake (ou l'extension Qt, pour des versions antiques de l'EDI) s'occupe d'appeler qui de droit au bon moment.
Avatar de Aurelien.Regat-Barrel Aurelien.Regat-Barrel - Expert éminent https://www.developpez.com
le 06/04/2016 à 10:14
Citation Envoyé par LittleWhite Voir le message
Pour le moc sur les projets VC++, jom.exe ne règle t-il pas les soucis ?
C'est ce qu'utilise QtCreator/QMake et c'est normalement QMake qui fait les fichiers des projets VS, donc cela devrait passer, non ?
Les projets VS sont en fait des fichiers MSBuild qui est le système de build de MS (VS est grosso modo un front end graphique pour MSBuild). Le souci c'est que par défaut, au niveau d'un même projet, les tâches de build ne sont pas parallélisées - les builds des projets le sont mais pas les tâches au sein des projets. L'exception à cela c'est les tâches de compilation car c'est le compilo qui se fork lui-même pour traiter les demandes en parallèle (et non MSBuild qui fait le boulot). Aussi pour des tâches genre moc, pas de parallélisation automatique. Et quand y'a des milliers de fichiers à moc'er, ben ça se sent! Pour s'en sortir il faut développer son propre outil qui va exécuter moc en parallèle et donc customiser la génération des projets au lieu de passer par qmake

Pour ne pas que le nombre de cpp à compiler double, la parade est de faire un include de son fichier moc depuis le code cpp. Ainsi qmake ne génère pas une step supplémentaire de build vu que le fichier généré est inclus dans le cpp d'origine. Ca marche pour toutes les plateformes, mais c'est un peu plus contraignant pour le programmeur. L'option plus "automatique" c'est de faire un unity build des fichiers moc générés pour les compiler tous d'un coup. Mais ça demande de développer un outil pour cela (il parait que CMake sait le faire).
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 12/04/2016 à 2:54
Salut,
Citation Envoyé par Aurelien.Regat-Barrel Voir le message
Les projets VS sont en fait des fichiers MSBuild qui est le système de build de MS (VS est grosso modo un front end graphique pour MSBuild). Le souci c'est que par défaut, au niveau d'un même projet, les tâches de build ne sont pas parallélisées - les builds des projets le sont mais pas les tâches au sein des projets. L'exception à cela c'est les tâches de compilation car c'est le compilo qui se fork lui-même pour traiter les demandes en parallèle (et non MSBuild qui fait le boulot). Aussi pour des tâches genre moc, pas de parallélisation automatique. Et quand y'a des milliers de fichiers à moc'er, ben ça se sent! Pour s'en sortir il faut développer son propre outil qui va exécuter moc en parallèle et donc customiser la génération des projets au lieu de passer par qmake
Autant le dire tout de suite, je ne suis plus sur du tout de mon coup sur ce point, mais, il me semble que des outils comme incredibuild (qui s'intègre très bien à visual studio) permet de partager tous les processus requis, y compris la création des moc...

Du moins, c'est ce dont je crois me souvenir de la dernière fois où je l'ai vu tourner

Mais bon, comme c'est un programme payant de compilation distribuée (et qu'il me semble que sa licence se monnaye à prix d'or), ce n'est pas forcément la meilleure solution à proposer non plus
Avatar de Aurelien.Regat-Barrel Aurelien.Regat-Barrel - Expert éminent https://www.developpez.com
le 12/04/2016 à 18:22
Hello,

Citation Envoyé par koala01 Voir le message
Autant le dire tout de suite, je ne suis plus sur du tout de mon coup sur ce point, mais, il me semble que des outils comme incredibuild (qui s'intègre très bien à visual studio) permet de partager tous les processus requis, y compris la création des moc...
Oui IncrediBuild sait paralléliser les custom build steps. Et l'outil a été intégré à Visual Studio depuis l'Update 1 comme "coordinateur" de build plus évolué que MSBuild (qui est assez mauvais au niveau task scheduling):
https://blogs.msdn.microsoft.com/vis...l-studio-2015/
Mais j'ai pas testé cette version (on a essayé la version full qui est effectivement assez onéreuse).

On a finalement pas retenu l'outil car le gain promis n'était pas là dans notre cas (on a un gros gros projet assez mal fichu il faut le reconnaître). Donc pour les mocs on utilise notre outil maison en pre-build step qui scanne les headers et appelle moc en parallèle dessus si besoin.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 12/04/2016 à 19:37
Je crois que cela dépend surtout du matériel que tu peux mettre à disposition de la bête...

Pour autant que mes souvenirs soient bons, incredibuild partageait systématiquement les tâches sur pas moins de 16 coeurs différents, ce qui permettait d'obtenir des temps de compilation "raisonnables", malgré la présence plusieurs milliers de fichier (au moins une bonne centaines nécessitant l'utilisation de moc)

Mais, encore une fois, une situation n'étant pas l'autre, ce qui était vrai pour le projet dont je parle ne l'est pas forcément pour le tien
Responsable bénévole de la rubrique Qt : Thibaut Cuvelier -