C'est une question un peu bizarre. Mes objectifs sont de comprendre la décision de conception du langage et d’identifier les possibilités de réflexion en C++.
Pourquoi le comité de langage C++ n'est pas allé à la mise en œuvre d'une réflexion dans le langage? La réflexion est-elle trop difficile dans un langage qui ne s'exécute pas sur une machine virtuelle (comme Java)?
Si l’on implémentait une réflexion sur le C++, quels seraient les défis?
Je suppose que les utilisations de la réflexion sont bien connues: les éditeurs peuvent être écrits plus facilement, le code de programme sera plus petit, des simulacres peuvent être générés pour les tests unitaires, etc. Mais ce serait bien si vous pouviez aussi commenter les utilisations de la réflexion.
Il y a plusieurs problèmes de réflexion en C++.
Il faut beaucoup de travail à ajouter, et le comité C++ est assez conservateur et ne passez pas de temps à explorer de nouvelles fonctionnalités radicales à moins d'être sûr que cela rapportera. (Une suggestion pour l’ajout d’un système de modules similaire à celui des assemblages .NET a été faite et, même si je pense qu’il est généralement admis qu’il serait agréable de l’avoir, ce n’est pas leur priorité absolue pour le moment, et a été repoussée bien après. C++ 0x La motivation de cette fonctionnalité est de supprimer le #include
système, mais il permettrait également au moins certaines métadonnées).
Vous ne payez pas pour ce que vous n'utilisez pas. C’est l’une des philosophies de conception de base à la base de C++. Pourquoi mon code devrait-il transporter des métadonnées si je n'en ai jamais besoin? De plus, l’ajout de métadonnées peut empêcher le compilateur d’optimiser. Pourquoi devrais-je payer ce coût dans mon code si je n'ai jamais besoin de ces métadonnées?
Ce qui nous amène à un autre gros point: C++ fait très peu de garanties sur le code compilé. Le compilateur est autorisé à faire à peu près tout ce qu’il aime, tant que la fonctionnalité résultante est celle attendue. Par exemple, vos classes ne sont pas obligées d'être réellement . Le compilateur peut les optimiser loin, en ligne, tout ce qu'ils font, et c'est souvent ce qu'ils font, car même un simple code de modèle a tendance à créer plusieurs instanciations de modèles. La bibliothèque standard C++ s'appuie sur cette optimisation agressive. Les foncteurs ne sont performants que si la surcharge liée à l'instanciation et à la destruction de l'objet peut être optimisée. operator[]
sur un vecteur ne peut être comparé qu’à l’indexation de tableaux bruts en termes de performances car l’opérateur tout entier peut être en ligne et ainsi entièrement supprimé du code compilé. C # et Java donnent beaucoup de garanties sur la sortie du compilateur. Si je définis une classe en C #, cette classe existera dans l’assemblée résultante, même si je ne l’utilise jamais, même si tous les appels à ses fonctions membres peuvent être en ligne. La classe doit être présente pour que la réflexion puisse la trouver. Une partie de cela est allégée par C # compiler en bytecode, ce qui signifie que le compilateur JIT peut supprimer les définitions de classe et les fonctions en ligne s’il le souhaite, même si le compilateur C # initial ne le peut pas. C++, vous n’avez qu’un compilateur et il doit générer un code efficace. Si vous étiez autorisé à inspecter les métadonnées d’un exécutable C++, vous vous attendriez à voir chaque classe définie, ce qui signifie que le compilateur devra conserver les classes définies, même si elles ne sont pas nécessaires.
Et puis il y a des modèles. Les modèles en C++ ne ressemblent en rien aux génériques d'autres langages. Chaque instanciation de modèle crée un type nouveau . std::vector<int>
est une classe complètement séparée de std::vector<float>
. Cela fait beaucoup de types différents dans un programme complet. Que devrait voir notre réflexion? Le modèle std::vector
? Mais comment peut-il, puisqu'il s'agit d'une construction de code source, qui n'a aucune signification au moment de l'exécution? Il faudrait voir les classes séparées std::vector<int>
et std::vector<float>
. Et std::vector<int>::iterator
et std::vector<float>::iterator
, pareil pour const_iterator
etc. Et une fois que vous entrez dans la métaprogrammation des modèles, vous finissez rapidement par instancier des centaines de modèles, qui sont tous alignés et supprimés à nouveau par le compilateur. Ils n'ont aucune signification, sauf dans le cadre d'un métaprogramme au moment de la compilation. Est-ce que toutes ces centaines de classes devraient être visibles à la réflexion? Ils devraient le faire, sinon notre réflexion serait inutile, si elle ne garantit même pas que les classes que j'ai définies seront effectivement présentes . Et un problème secondaire est que la classe de modèle n'existe pas tant qu'elle n'est pas instanciée. Imaginez un programme qui utilise std::vector<int>
. Notre système de réflexion devrait-il pouvoir voir std::vector<int>::iterator
? D'une part, vous vous en doutez bien. C'est une classe importante, définie en termes de std::vector<int>
, qui existe dans les métadonnées. D'un autre côté, si le programme n'utilise jamais réellement ce modèle de classe d'itérateur , son type n'aura jamais été instancié et le compilateur n'aura donc pas généré la classe en premier lieu. Et il est trop tard pour le créer au moment de l'exécution, car il nécessite un accès au code source.
boost::type_traits
est un exemple simple. Vous voulez savoir sur le type T
? Vérifiez sa type_traits
. En C #, il vous faudrait pêcher après son type en utilisant la réflexion. La réflexion serait toujours utile pour certaines choses (l'utilisation principale que je vois, que la métaprogrammation ne peut pas remplacer facilement, concerne le code de sérialisation auto-généré), mais elle entraînerait des coûts importants pour le C++, et ce n'est tout simplement pas nécessaire aussi souvent est dans d'autres langues.Modifier: En réponse aux commentaires:
cdleary: Oui, les symboles de débogage font quelque chose de similaire, en ce sens qu'ils stockent des métadonnées sur les types utilisés dans l'exécutable. Mais ils souffrent aussi des problèmes que j'ai décrits. Si vous avez déjà essayé de déboguer une version, vous comprendrez ce que je veux dire. Il existe de grandes lacunes logiques dans lesquelles vous avez créé une classe dans le code source, qui a été insérée dans le code final. Si vous utilisiez la réflexion pour quelque chose d’utile, vous en auriez besoin pour être plus fiable et cohérente. Dans l’état actuel des choses, les types disparaissent presque chaque fois que vous compilez. Vous modifiez un tout petit détail et le compilateur décide de modifier les types en ligne et ceux qui ne le sont pas, en réponse. Comment pouvez-vous extraire quelque chose d'utile de cela, alors que vous n'êtes même pas assuré que les types les plus pertinents seront représentés dans vos métadonnées? Le type que vous recherchiez était peut-être là dans la dernière version, mais il est maintenant parti. Et demain, quelqu'un enregistrera un petit changement innocent dans une petite fonction innocente, ce qui rend le type assez grand pour qu'il ne soit pas complètement en ligne, il sera donc de retour. Cela reste utile pour les symboles de débogage, mais pas beaucoup plus que cela. Je détesterais essayer de générer du code de sérialisation pour une classe sous ces termes.
Evan Teran: Bien sûr, ces problèmes pourraient être résolus. Mais cela revient à mon point n ° 1. Cela prendrait beaucoup de travail et le comité C++ a beaucoup de choses qui, à son avis, sont plus importantes. L’avantage d’obtenir une réflexion limitée (et le serait-il limité) en C++ est-il vraiment assez important pour justifier de se concentrer sur cela au détriment d’autres fonctionnalités? Y at-il vraiment un avantage énorme à ajouter des fonctionnalités au langage principal qui peuvent déjà (principalement) être effectuées via des bibliothèques et des pré-processeurs tels que QT? Peut-être, mais le besoin est beaucoup moins urgent que si de telles bibliothèques n'existaient pas. Pour vos suggestions spécifiques cependant, je pense que le refuser sur des modèles le rendrait complètement inutile. Vous seriez incapable d'utiliser la réflexion sur la bibliothèque standard, par exemple. Quel genre de réflexion ne vous laisserait pas voir un std::vector
? Les modèles sont une énorme partie de C++. Une fonctionnalité qui ne fonctionne pas sur les modèles est fondamentalement inutile.
Mais vous avez raison, une forme de réflexion pourrait être mise en œuvre. Mais ce serait un changement majeur dans la langue. Dans l'état actuel des choses, les types sont exclusivement une construction à la compilation. Ils existent pour le bénéfice du compilateur, et rien d’autre. Une fois le code compilé, il n'y a pas de classes . Si vous vous étirez trop, vous pourriez affirmer que les fonctions existent toujours, mais en réalité, tout ce qu'il y a, c'est un tas d'instructions d'assembleur de sauts, et beaucoup de piles Push/Pop. Il n'y a pas grand chose à faire en ajoutant de telles métadonnées.
Mais comme je l’ai dit, il est proposé de modifier le modèle de compilation, d’ajouter des modules autonomes, de stocker des métadonnées pour certains types, de permettre à d’autres modules de les référencer sans avoir à manipuler #include
s. C'est un bon début, et pour être honnête, je suis surpris que le comité de normalisation ne se soit pas contenté de rejeter la proposition en faveur d'un changement trop important. Alors peut-être dans 5-10 ans? :)
Reflection nécessite que certaines métadonnées sur les types soient stockées dans un endroit pouvant être interrogé. Étant donné que C++ est compilé en code machine natif et soumis à des modifications importantes en raison de l'optimisation, la vue de haut niveau de l'application est pratiquement perdue lors de la compilation. Par conséquent, il n'est pas possible de les interroger au moment de l'exécution. Java et .Net Utilisez une représentation de très haut niveau dans le code binaire pour les machines virtuelles rendant ce niveau de réflexion possible. Cependant, dans certaines implémentations C++, il existe quelque chose appelé Informations de type à l'heure d'exécution (RTTI). ) qui peut être considéré comme une version simplifiée de la réflexion.
Toutes les langues ne doivent pas essayer d’intégrer toutes les fonctionnalités de toutes les autres langues.
C++ est essentiellement un assembleur de macros très, très sophistiqué. Ce n'est pas (au sens traditionnel) un langage de haut niveau comme C #, Java, Objective-C, Smalltalk, etc.
Il est bon d’avoir différents outils pour différents emplois. Si nous avons seulement des marteaux, tout va ressembler à des clous, etc. Avoir des langages de script est utile pour certains travaux, et des langages OO réfléchissants (Java, Obj-C, C #) sont utiles pour une autre classe de travaux, et super Des langages proches de la machine, simples et efficaces, sont utiles pour une autre classe de travaux (C++, C, Assembler).
C++ accomplit un travail incroyable en étendant la technologie d'Assembler à des niveaux incroyables de gestion de la complexité et d'abstractions pour rendre la programmation de tâches plus grandes et plus complexes plus possibles pour l'homme. Mais ce n’est pas nécessairement le langage le mieux adapté à ceux qui abordent leur problème dans une perspective de haut niveau (LISP, Smalltalk, Java, C #). Si vous avez besoin d'un langage doté de ces fonctionnalités pour mettre en œuvre au mieux une solution à vos problèmes, remerciez ceux qui ont créé ces langages pour que nous puissions tous les utiliser!
Mais C++ est destiné à ceux qui, pour une ou plusieurs raisons, ont besoin d’une forte corrélation entre leur code et le fonctionnement de la machine sous-jacente. Qu'il s'agisse d'efficacité, de pilotes de périphérique de programmation, d'interaction avec les services de système d'exploitation de niveau inférieur ou autre, le C++ convient mieux à ces tâches.
C #, Java, Objective-C ont tous besoin d’un système d’exécution beaucoup plus volumineux et plus riche pour prendre en charge leur exécution. Cette exécution doit être livrée au système en question - préinstallée pour prendre en charge le fonctionnement de votre logiciel. Et cette couche doit être gérée pour différents systèmes cibles, personnalisée par QUELQUE AUTRE LANGUE pour la faire fonctionner sur cette plate-forme. Et cette couche intermédiaire - cette couche adaptative entre le système d'exploitation hôte et votre code - le runtime, est presque toujours écrite dans un langage tel que C ou C++, où l'efficacité est la première des solutions, car la compréhension prévisible de l'interaction exacte entre logiciel et matériel compris et manipulé au maximum.
J'adore Smalltalk, Objective-C, et disposer d'un système d'exécution riche avec réflexion, métadonnées, récupération de place, etc. Un code incroyable peut être écrit pour tirer parti de ces fonctionnalités! Mais c'est simplement une couche supérieure sur la pile, une couche qui doit reposer sur des couches inférieures, qui elles-mêmes doivent finalement reposer sur le système d'exploitation et le matériel. Et nous aurons toujours besoin du langage le mieux adapté pour construire cette couche: C++/C/Assembler.
Addendum: C++ 11/14 continuent d'étendre la capacité de C++ à prendre en charge des systèmes d'abstractions et de niveaux supérieurs. Les threads, la synchronisation, des modèles de mémoire précis, des définitions de machine abstraites plus précises permettent aux développeurs C++ de réaliser de nombreuses abstractions de haut niveau que certains de ces langages de haut niveau ne comportaient auparavant que sur des domaines exclusifs, tout en continuant à fournir des fonctions proches de performances en métal et excellente prévisibilité (c.-à-d. sous-systèmes d'exécution minimaux). Peut-être que les possibilités de réflexion seront activées de manière sélective dans une future révision du C++, pour ceux qui le souhaitent - ou peut-être une bibliothèque fournira-t-elle de tels services d'exécution (peut-être qu'il y en a un maintenant, ou que le début en est un de boost?).
Si vous voulez vraiment comprendre les décisions de conception relatives au C++, recherchez une copie du Manuel de référence du C++ annoté par Ellis et Stroustrup. Ce n'est pas à jour avec la dernière norme, mais elle reprend la norme d'origine et explique comment les choses fonctionnent et souvent, comment elles sont devenues ainsi.
La réflexion pour les langues qui le concernent dépend de la quantité de code source que le compilateur est disposé à laisser dans votre code d'objet pour permettre la réflexion et de la quantité d'outils d'analyse disponibles pour interpréter les informations reflétées. À moins que le compilateur ne conserve tout le code source, la capacité d'analyse des données disponibles sur le code source sera limitée.
Le compilateur C++ ne conserve rien (eh bien, en ignorant RTTI), ainsi vous ne recevez pas de réflexion dans le langage. (Les compilateurs Java et C # conservent uniquement les noms de classe, de méthode et les types retournés. Vous obtenez donc un peu de données de réflexion, mais vous ne pouvez pas inspecter les expressions ni la structure du programme. les informations que vous pouvez obtenir sont assez rares et par conséquent vous ne pouvez vraiment pas faire beaucoup d’analyses).
Mais vous pouvez sortir de l'extérieur du langage et obtenir des capacités de réflexion complètes. La réponse à une autre discussion de débordement de pile sur réflexion en C en discute.
La réflexion peut être et a été implémentée dans c ++ auparavant.
Ce n'est pas une fonctionnalité native de c ++ car elle a un coût important (mémoire et vitesse) qui ne devrait pas être défini par défaut par le langage - le langage est orienté "performances maximales par défaut".
Comme vous ne devriez pas payer pour ce dont vous n’avez pas besoin, et comme vous le dites vous-même, il est plus nécessaire dans les éditeurs que dans les autres applications, alors cela devrait être implémenté seulement où vous en avez besoin, et non "forcé" à tout le code ( vous n'avez pas besoin de réfléchir sur toutes les données avec lesquelles vous travaillerez dans un éditeur ou une application similaire).
La raison pour laquelle C++ n'a pas réfléchi est que cela impliquerait que les compilateurs ajoutent des informations sur les symboles aux fichiers objets, comme ce que possèdent les membres d'un type de classe, des informations sur les membres, sur les fonctions, etc. Cela rendrait essentiellement les fichiers d'inclusion inutiles, car les informations fournies par les déclarations seraient alors lues à partir de ces fichiers objets (modules ensuite). En C++, une définition de type peut se produire plusieurs fois dans un programme en incluant les en-têtes respectifs (à condition que toutes ces définitions soient identiques), il faudrait donc décider où placer les informations sur ce type, comme pour en nommer une. complication ici. L'optimisation agressive effectuée par un compilateur C++, capable d'optimiser des dizaines d'instanciations de modèles de classe, constitue un autre point fort. C'est possible, mais comme le C++ est compatible avec le C, cela deviendrait une combinaison maladroite.
Il existe de nombreux cas d'utilisation de la réflexion en C++ qui ne peuvent pas être traités de manière adéquate à l'aide de constructions de compilation telles que la méta-programmation de modèles.
N334 propose des pointeurs riches comme moyen d'introduire la réflexion en C++. Entre autres choses, il traite de la question de ne pas payer pour une fonctionnalité à moins que vous ne l'utilisiez.
Si C++ pouvait avoir:
const
const
Cela suffirait pour créer des bibliothèques très faciles à utiliser au cœur du traitement de données sans typage qui est si répandu dans les applications Web et de base de données actuelles (tous les orms, mécanismes de messagerie, analyseurs syntaxiques xml/json, sérialisation de données, etc.).
Par exemple, les informations de base prises en charge par le Q_PROPERTY
macro (partie de Qt Framework) http://qt.nokia.com/doc/4.5/properties.html développé pour couvrir les méthodes de classe et e) - serait extrêmement bénéfique pour C++ et pour la communauté des logiciels en général.
Certes, la réflexion à laquelle je fais allusion ne couvrirait pas le sens sémantique ni des questions plus complexes (comme les numéros de ligne des codes de commentaires, l'analyse des flux de données, etc.) - mais je ne pense pas non plus que celles-ci soient nécessaires pour faire partie d'une norme de langage.
Selon Alistair Cockburn, le sous-typage ne peut pas être garanti dans un environnement de réflexion .
La réflexion est plus pertinente pour les systèmes de typage latent. En C++, vous savez quel type vous avez et ce que vous pouvez en faire.
La réflexion pourrait être facultative, comme une directive de préprocesseur. Quelque chose comme
#pragma enable reflection
De cette façon, nous pouvons avoir le meilleur des deux mondes, sans ce pragma, les bibliothèques seraient créées sans réflexion (sans aucuns frais généraux comme indiqué), ce serait alors au développeur individuel qu’il souhaite la rapidité ou la facilité d’utilisation.
quelques bons liens sur la réflexion en C++ je viens de trouver:
Document de travail de la norme C++: Aspects de la réflexion en C++
C'est essentiellement parce que c'est un "extra optionnel". De nombreuses personnes choisissent C++ plutôt que des langages tels que Java et C #) pour pouvoir mieux contrôler la sortie du compilateur, par exemple un programme plus petit et/ou plus rapide.
Si vous choisissez d'ajouter une réflexion, il y a différentes solutions disponibles .
Je pense que la réflexion en C++ est d’une importance cruciale si le C++ doit être utilisé comme langage pour l’accès à la base de données, la gestion de session Web/le développement http et l’interface graphique. Le manque de réflexion empêche les ORM (comme Hibernate ou LINQ), les analyseurs syntaxiques XML et JSON qui instancient des classes, la sérialisation des données et de nombreux autres thigns (où des données initialement sans type doivent être utilisées pour créer une instance de classe).
Un sélecteur de temps de compilation disponible pour un développeur de logiciel pendant le processus de construction peut être utilisé pour éliminer ce problème "vous payez pour ce que vous utilisez".
Je n’ai pas besoin de la réflexion pour lire des données à partir d’un port série. Dans ce cas, n’utilisez pas le commutateur. Mais en tant que développeur de base de données qui souhaite continuer à utiliser le C++, je suis constamment en phase avec un code horrible et difficile à gérer qui mappe les données entre les membres de données et les constructions de base de données.
Ni la sérialisation Boost ni un autre mécanisme ne résout réellement la réflexion - cela doit être fait par le compilateur - et une fois que ce sera fait, le C++ sera à nouveau appris dans les écoles et utilisé dans les logiciels traitant de l'informatique.
Pour moi, ce problème n ° 1 (et les primitives de threading naitives est le problème n ° 2).