web-dev-qa-db-fra.com

Pourquoi le «polymorphisme pur» est-il préférable à l'utilisation du RTTI?

Presque toutes les ressources C++ que j'ai vues qui discutent de ce genre de choses me disent que je devrais préférer les approches polymorphes à l'utilisation de RTTI (identification de type au moment de l'exécution). En général, je prends ce type de conseil au sérieux et j'essaierai de comprendre la raison d'être - après tout, le C++ est une bête puissante et difficile à comprendre dans toute sa profondeur. Cependant, pour cette question particulière, je dessine un blanc et je voudrais voir quel genre de conseils Internet peut offrir. Tout d'abord, permettez-moi de résumer ce que j'ai appris jusqu'à présent, en énumérant les raisons courantes citées pour lesquelles RTTI est "considéré comme nuisible":

Certains compilateurs ne l'utilisent pas/RTTI n'est pas toujours activé

Je n'achète vraiment pas cet argument. C'est comme dire que je ne devrais pas utiliser les fonctionnalités C++ 14, car il existe des compilateurs qui ne le prennent pas en charge. Et pourtant, personne ne me découragerait d'utiliser les fonctionnalités C++ 14. La majorité des projets auront une influence sur le compilateur qu'ils utilisent et sur la façon dont il est configuré. Même en citant la page de manuel gcc:

-fno-rtti

Désactivez la génération d'informations sur chaque classe avec des fonctions virtuelles à utiliser par les fonctionnalités d'identification de type d'exécution C++ (dynamic_cast et typeid). Si vous n'utilisez pas ces parties de la langue, vous pouvez économiser de l'espace en utilisant cet indicateur. Notez que la gestion des exceptions utilise les mêmes informations, mais G ++ les génère selon les besoins. L'opérateur dynamic_cast peut toujours être utilisé pour les conversions qui ne nécessitent pas d'informations de type au moment de l'exécution, c'est-à-dire les conversions en "void *" ou en classes de base non ambiguës.

Ce que cela me dit, c'est que si Je n'utilise pas RTTI, je peux le désactiver. C'est comme dire que si vous n'utilisez pas Boost, vous n'avez pas besoin de vous y connecter. Je n'ai pas à planifier le cas où quelqu'un compile avec -fno-rtti. De plus, le compilateur échouera fort et clair dans ce cas.

Cela coûte plus de mémoire/peut être lent

Chaque fois que je suis tenté d'utiliser RTTI, cela signifie que je dois accéder à une sorte d'informations de type ou à un trait de ma classe. Si j'implémente une solution qui n'utilise pas RTTI, cela signifie généralement que je devrai ajouter des champs à mes classes pour stocker ces informations, donc l'argument de la mémoire est un peu nul (je vais en donner un exemple plus loin).

Un dynamic_cast peut être lent, en effet. Cependant, il existe généralement des moyens d'éviter d'avoir à l'utiliser dans des situations critiques. Et je ne vois pas vraiment l'alternative. Cette réponse SO suggère d'utiliser une énumération, définie dans la classe de base, pour stocker le type. Cela ne fonctionne que si vous connaissez toutes vos classes dérivées a priori. C'est un gros "si"!

De cette réponse, il semble également que le coût du RTTI ne soit pas clair non plus. Différentes personnes mesurent différentes choses.

Les conceptions polymorphes élégantes rendront le RTTI inutile

C'est le genre de conseils que je prends au sérieux. Dans ce cas, je ne peux tout simplement pas trouver de bonnes solutions non RTTI qui couvrent mon cas d'utilisation RTTI. Permettez-moi de vous donner un exemple:

Disons que j'écris une bibliothèque pour gérer les graphiques de certains types d'objets. Je veux permettre aux utilisateurs de générer leurs propres types lors de l'utilisation de ma bibliothèque (donc la méthode enum n'est pas disponible). J'ai une classe de base pour mon nœud:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

Maintenant, mes nœuds peuvent être de différents types. Que penses-tu de ceux-ci:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

Enfer, pourquoi pas même l'un d'eux:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

La dernière fonction est intéressante. Voici un moyen de l'écrire:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

Tout cela semble clair et net. Je n'ai pas à définir d'attributs ou de méthodes là où je n'en ai pas besoin, la classe de nœud de base peut rester légère et moyenne. Sans RTTI, par où commencer? Je peux peut-être ajouter un attribut node_type à la classe de base:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

Est-ce que std :: string est une bonne idée pour un type? Peut-être pas, mais que puis-je utiliser d'autre? Inventer un numéro et espérer que personne d'autre ne l'utilise encore? De plus, dans le cas de mon orange_node, que faire si je veux utiliser les méthodes de red_node et yellow_node? Dois-je stocker plusieurs types par nœud? Cela semble compliqué.

Conclusion

Ces exemples ne semblent pas trop complexes ou inhabituels (je travaille sur quelque chose de similaire dans mon travail de jour, où les nœuds représentent du matériel réel qui est contrôlé par le logiciel, et qui font des choses très différentes selon ce qu'ils sont). Pourtant, je ne connais pas une façon propre de le faire avec des modèles ou d'autres méthodes. Veuillez noter que j'essaie de comprendre le problème, pas de défendre mon exemple. Ma lecture de pages telles que la réponse SO que j'ai liée ci-dessus et cette page sur Wikibooks semble suggérer que j'utilise abusivement RTTI, mais je voudrais savoir pourquoi.

Donc, revenons à ma question initiale: Pourquoi le `` polymorphisme pur '' est-il préférable à l'utilisation de RTTI?

104
mbr0wn

Une interface décrit ce que l'on doit savoir pour interagir dans une situation donnée en code. Une fois que vous étendez l'interface avec "toute votre hiérarchie de types", la "surface" de votre interface devient énorme, ce qui rend le raisonnement à ce sujet plus difficile.

À titre d'exemple, votre "poke adjacent oranges" signifie que moi, en tant que tierce partie, je ne peux pas imiter être une orange! Vous avez déclaré en privé un type orange, puis utilisez RTTI pour que votre code se comporte de manière spéciale lors de l'interaction avec ce type. Si je veux "être orange", je doit être dans votre jardin privé.

Maintenant, tout le monde qui se couple avec "orangeté" se couple avec tout votre type d'orange, et implicitement avec tout votre jardin privé, plutôt qu'avec une interface définie.

À première vue, cela semble être un excellent moyen d'étendre l'interface limitée sans avoir à changer tous les clients (en ajoutant am_I_orange), ce qui a tendance à se produire à la place est qu'il ossifie la base de code, et empêche une extension supplémentaire. L'orangeté particulière devient inhérente au fonctionnement du système, et vous empêche de créer un remplacement "orange" pour l'orange qui est implémenté différemment et peut-être supprime une dépendance ou résout élégamment un autre problème.

Cela signifie que votre interface doit être suffisante pour résoudre votre problème. De ce point de vue, pourquoi avez-vous besoin de piquer uniquement des oranges, et si oui, pourquoi l'orangosité n'était-elle pas disponible dans l'interface? Si vous avez besoin d'un ensemble de balises floues qui peuvent être ajoutées ad-hoc, vous pouvez l'ajouter à votre type:

class node_base {
  public:
    bool has_tag(tag_name);

Cela fournit un élargissement massif similaire de votre interface, de étroitement spécifié à large basé sur des balises. Sauf qu'au lieu de le faire via RTTI et les détails d'implémentation (aka, "comment êtes-vous implémenté? Avec le type orange? Ok vous passez.")), Il le fait avec quelque chose de facilement émulé à travers un complètement mise en œuvre différente.

Cela peut même être étendu aux méthodes dynamiques, si vous en avez besoin. "Est-ce que tu soutiens être Foo'd avec des arguments Baz, Tom et Alice? Ok, Fooing you." Dans un grand sens, c'est moins intrusif qu'une distribution dynamique pour comprendre que l'autre objet est un type que vous connaissez.

Désormais, les objets mandarine peuvent avoir la balise orange et jouer, tout en étant découplés de l'implémentation.

Cela peut toujours conduire à un énorme gâchis, mais c'est au moins un gâchis de messages et de données, pas des hiérarchies d'implémentation.

L'abstraction est un jeu de découplage et de dissimulation de non-pertinence. Cela rend le code plus facile à raisonner localement. RTTI perce un trou directement dans l'abstraction dans les détails de mise en œuvre. Cela peut faciliter la résolution d'un problème, mais cela a le coût de vous enfermer très facilement dans une implémentation spécifique.

68

La plupart des persuasions morales contre telle ou telle caractéristique sont la typicité provient de l'observation qu'il existe une ombre de utilisations mal conçues de cette fonctionnalité.

Là où les moralistes échouent, c'est qu'ils supposent que TOUS les usages sont mal conçus, tandis que en fait les fonctionnalités existent pour une raison.

Ils ont ce que j'appelais le "complexe du plombier": ils pensent que tous les robinets sont défectueux parce que tous les robinets qu'ils sont appelés à réparer le sont. La réalité est que la plupart des robinets fonctionnent bien: vous n'appelez tout simplement pas de plombier pour eux!

Une chose folle qui peut arriver, c'est lorsque, pour éviter d'utiliser une fonctionnalité donnée, les programmeurs écrivent beaucoup de code standard réimplémentant en privé exactement cette fonctionnalité. (Avez-vous déjà rencontré des classes qui n'utilisent pas RTTI ni d'appels virtuels, mais ont une valeur pour suivre de quel type dérivé réel s'agit-il? Ce n'est pas plus que [~ # ~] rtti [~ # ~] réinvention déguisée.)

Il existe une manière générale de penser au polymorphisme: IF(selection) CALL(something) WITH(parameters). (Désolé, mais la programmation, quand on ne tient pas compte de l'abstraction, c'est tout cela)

L'utilisation du polymorphisme au moment de la conception (concepts) à la compilation (basé sur la déduction de modèle), au moment de l'exécution (basé sur l'héritage et les fonctions virtuelles) ou piloté par les données (RTTI et commutation), dépend de la quantité de décisions connues. à chacune des étapes de la production et comment variable ils sont à chaque contexte.

L'idée est que:

plus vous pouvez anticiper, meilleures sont les chances d'attraper des erreurs et d'éviter les bogues affectant l'utilisateur final.

Si tout est constant (y compris les données), vous pouvez tout faire avec la méta-programmation de modèles. Une fois la compilation effectuée sur les constantes actualisées, l'ensemble du programme se résume à une simple déclaration de retour qui crache le résultat .

S'il existe un certain nombre de cas connus au moment de la compilation , mais que vous ne connaissez pas les données réelles sur lesquelles ils doivent agir, alors le polymorphisme à la compilation (principalement CRTP ou similaire) peut être une solution.

Si la sélection des cas dépend des données (pas de valeurs connues au moment de la compilation) et que la commutation est unidimensionnelle (que faire peut être réduite à une seule valeur), alors répartition basée sur les fonctions virtuelles (ou en général "tables de pointeurs de fonctions" ") est nécessaire.

Si la commutation est multidimensionnelle, étant donné qu'aucune répartition d'exécution native multiple n'existe en C++, vous devez alors:

  • Réduisez à une dimension par Goedelization : c'est là que les bases virtuelles et l'héritage multiple, avec diamants et les parallélogrammes empilés le sont, mais cela nécessite que le nombre de combinaisons possibles soit connu et relativement petit.
  • Enchaînez les dimensions l'une dans l'autre (comme dans le modèle composite-visiteurs, mais cela nécessite que toutes les classes soient conscientes de leurs autres frères et sœurs, donc il ne peut pas "évoluer" à partir de l'endroit où il a été conçu)
  • Envoyez des appels en fonction de plusieurs valeurs. C'est exactement à ça que sert RTTI.

Si ce n'est pas seulement la commutation, mais même que les actions ne sont pas connues au moment de la compilation, alors script et analyse est requis: les données elles-mêmes doivent décrire l'action à exécuter pris sur eux.

Maintenant, puisque chacun des cas que j'ai énumérés peut être considéré comme un cas particulier de ce qui suit, vous pouvez résoudre chaque problème en abusant de la solution la plus basse pour les problèmes abordables avec le plus haut.

C'est ce que la moralisation pousse réellement à éviter. Mais cela ne signifie pas que les problèmes qui vivent dans les domaines les plus bas n'existent pas!

Bashing RTTI juste pour le bash, c'est comme bashing goto juste pour le bash. Des choses pour les perroquets, pas pour les programmeurs.

31

Cela a l'air plutôt soigné dans un petit exemple, mais dans la vraie vie, vous vous retrouverez bientôt avec un long ensemble de types qui peuvent se piquer, certains d'entre eux peut-être seulement dans une direction.

Qu'en est-il de dark_orange_node, Ou black_and_orange_striped_node, Ou dotted_node? Peut-il avoir des points de couleurs différentes? Et si la plupart des points sont orange, peut-on alors les fourrer?

Et chaque fois que vous devez ajouter une nouvelle règle, vous devrez revoir toutes les fonctions poke_adjacent Et ajouter plus d'instructions if.


Comme toujours, il est difficile de créer des exemples génériques, je vais vous donner cela.

Mais si je devais faire cet exemple spécifique, j'ajouterais un membre poke() à toutes les classes et laisserais certains d'entre eux ignorer l'appel (void poke() {}) s'ils ne sont pas intéressés.

Ce serait sûrement encore moins cher que de comparer les typeids.

23
Bo Persson

Certains compilateurs ne l'utilisent pas/RTTI n'est pas toujours activé

Je pense que vous avez mal compris de tels arguments.

Il existe un certain nombre d'endroits de codage C++ où RTTI ne doit pas être utilisé. Où les commutateurs du compilateur sont utilisés pour désactiver de force RTTI. Si vous codez dans un tel paradigme ... alors vous avez presque certainement déjà été informé de cette restriction.

Le problème est donc avec les bibliothèques . Autrement dit, si vous écrivez une bibliothèque qui dépend de RTTI, votre bibliothèque ne peut pas être utilisée par les utilisateurs qui désactivent RTTI. Si vous souhaitez que votre bibliothèque soit utilisée par ces personnes, elle ne peut pas utiliser RTTI, même si votre bibliothèque est également utilisée par des personnes qui peuvent utiliser RTTI. Tout aussi important, si vous ne pouvez pas utiliser RTTI, vous devez magasiner un peu plus pour les bibliothèques, car l'utilisation de RTTI est une rupture pour vous.

Cela coûte plus de mémoire/peut être lent

Il y a beaucoup de choses que vous ne faites pas dans les boucles chaudes. Vous n'allouez pas de mémoire. Vous ne parcourez pas les listes liées. Et ainsi de suite. RTTI peut certainement être une autre de ces choses "ne fais pas ça ici".

Cependant, considérez tous vos exemples RTTI. Dans tous les cas, vous avez un ou plusieurs objets de type indéterminé et vous souhaitez effectuer sur eux des opérations qui peuvent ne pas être possibles pour certains d'entre eux.

C'est quelque chose que vous devez contourner au niveau de la conception . Vous pouvez écrire des conteneurs qui n'allouent pas de mémoire et qui correspondent au paradigme "STL". Vous pouvez éviter les structures de données de liste liée ou limiter leur utilisation. Vous pouvez réorganiser des tableaux de structures en structures de tableaux ou autre. Cela change certaines choses, mais vous pouvez le garder compartimenté.

Changer une opération RTTI complexe en un appel de fonction virtuel normal? C'est un problème de conception. Si vous devez changer cela, alors c'est quelque chose qui nécessite des changements à chaque classe dérivée . Il change la façon dont beaucoup de code interagit avec différentes classes. La portée d'un tel changement s'étend bien au-delà des sections de code critiques pour les performances.

Alors ... pourquoi l'avez-vous mal écrit au départ?

Je n'ai pas à définir d'attributs ou de méthodes là où je n'en ai pas besoin, la classe de nœud de base peut rester légère et moyenne.

À quelle fin?

Vous dites que la classe de base est "maigre et méchante". Mais vraiment ... c'est inexistant . Il ne fait rien .

Regardez votre exemple: node_base. Qu'Est-ce que c'est? Cela semble être une chose qui a des choses adjacentes. Il s'agit d'une interface Java (pré-génériques Java à cela)): une classe qui existe uniquement pour être quelque chose que les utilisateurs peuvent diffuser sur le type réel . Peut-être que vous ajoutez une fonctionnalité de base comme la contiguïté (Java ajoute ToString), mais c'est tout.

Il y a une différence entre "maigre et moyen" et "transparent".

Comme l'a dit Yakk, ces styles de programmation se limitent à l'interopérabilité, car si toutes les fonctionnalités sont dans une classe dérivée, les utilisateurs en dehors de ce système, sans accès à cette classe dérivée, ne peuvent pas interagir avec le système. Ils ne peuvent pas remplacer les fonctions virtuelles et ajouter de nouveaux comportements. Ils ne peuvent même pas appeler ces fonctions.

Mais ce qu'ils font aussi, c'est qu'il est très difficile de faire de nouvelles choses, même au sein du système. Considérez votre fonction poke_adjacent_oranges. Que se passe-t-il si quelqu'un veut un type Lime_node Qui peut être piqué comme les orange_node? Eh bien, nous ne pouvons pas dériver Lime_node De orange_node; ça n'a aucun sens.

Au lieu de cela, nous devons ajouter un nouveau Lime_node Dérivé de node_base. Modifiez ensuite le nom de poke_adjacent_oranges En poke_adjacent_pokables. Et puis, essayez de caster sur orange_node Et Lime_node; quel que soit le casting, c'est celui que nous poussons.

Cependant, Lime_node A besoin d'être propre poke_adjacent_pokables. Et cette fonction doit faire les mêmes vérifications de casting.

Et si nous ajoutons un troisième type, nous devons non seulement ajouter sa propre fonction, mais nous devons changer les fonctions dans les deux autres classes.

Évidemment, maintenant vous faites de poke_adjacent_pokables Une fonction gratuite, de sorte qu'elle fonctionne pour tous. Mais que pensez-vous qu'il se passe si quelqu'un ajoute un quatrième type et oublie de l'ajouter à cette fonction?

Bonjour, rupture silencieuse . Le programme semble fonctionner plus ou moins bien, mais ce n'est pas le cas. Si poke avait été une fonction virtuelle réelle , le compilateur aurait échoué si vous n'aviez pas remplacé la fonction virtuelle pure de node_base.

À votre façon, vous n'avez pas de telles vérifications du compilateur. Oh bien sûr, le compilateur ne vérifiera pas les virtuels non purs, mais au moins vous avez une protection dans les cas où la protection est possible (c'est-à-dire qu'il n'y a pas d'opération par défaut).

L'utilisation de classes de base transparentes avec RTTI conduit à un cauchemar de maintenance. En effet, la plupart des utilisations de RTTI conduisent à des maux de tête de maintenance. Cela ne signifie pas que RTTI n'est pas utile (c'est vital pour faire fonctionner boost::any, Par exemple). Mais c'est un outil très spécialisé pour des besoins très spécialisés.

De cette façon, il est "nuisible" de la même manière que goto. C'est un outil utile qui ne devrait pas être supprimé. Mais son utilisation devrait être rare dans votre code.


Donc, si vous ne pouvez pas utiliser des classes de base transparentes et un casting dynamique, comment éviter les interfaces lourdes? Comment éviter de propulser toutes les fonctions que vous voudrez peut-être appeler sur un type de propulser jusqu'à la classe de base?

La réponse dépend de la destination de la classe de base.

Les classes de base transparentes comme node_base Utilisent simplement le mauvais outil pour le problème. Les listes liées sont mieux gérées par les modèles. Le type de nœud et la contiguïté seraient fournis par un type de modèle. Si vous voulez mettre un type polymorphe dans la liste, vous le pouvez. Utilisez simplement BaseClass* Comme T dans l'argument du modèle. Ou votre pointeur intelligent préféré.

Mais il existe d'autres scénarios. L'un est un type qui fait beaucoup de choses, mais qui a des parties facultatives. Une instance particulière pourrait implémenter certaines fonctions, tandis qu'une autre ne le ferait pas. Cependant, la conception de tels types offre généralement une réponse appropriée.

La classe "entité" en est un parfait exemple. Cette classe a longtemps tourmenté les développeurs de jeux. Conceptuellement, il possède une interface gigantesque, vivant à l'intersection de près d'une douzaine de systèmes entièrement disparates. Et différentes entités ont des propriétés différentes. Certaines entités n'ont aucune représentation visuelle, donc leurs fonctions de rendu ne font rien. Et tout cela est déterminé lors de l'exécution.

La solution moderne pour cela est un système de type composant. Entity est simplement un conteneur d'un ensemble de composants, avec un peu de colle entre eux. Certains composants sont facultatifs; une entité qui n'a pas de représentation visuelle n'a pas le composant "graphiques". Une entité sans IA n'a pas de composant "contrôleur". Et ainsi de suite.

Les entités d'un tel système ne sont que des pointeurs vers des composants, la plupart de leur interface étant fournie en accédant directement aux composants.

Le développement d'un tel système de composants nécessite de reconnaître, au stade de la conception, que certaines fonctions sont regroupées conceptuellement, de sorte que tous les types qui en implémentent un les implémentent tous. Cela vous permet d'extraire la classe de la classe de base potentielle et d'en faire un composant distinct.

Cela permet également de suivre le principe de responsabilité unique. Une telle classe de composants n'a que la responsabilité d'être titulaire de composants.


De Matthew Walton:

Je note que beaucoup de réponses ne notent pas l'idée que votre exemple suggère que node_base fait partie d'une bibliothèque et que les utilisateurs créeront leurs propres types de nœuds. Ensuite, ils ne peuvent pas modifier node_base pour autoriser une autre solution, alors peut-être que RTTI devient alors leur meilleure option.

OK, explorons cela.

Pour que cela ait un sens, ce que vous devriez avoir est une situation où une bibliothèque L fournit un conteneur ou un autre support structuré de données. L'utilisateur peut ajouter des données à ce conteneur, parcourir son contenu, etc. Cependant, la bibliothèque ne fait vraiment rien avec ces données; il gère simplement son existence.

Mais il ne gère même pas son existence autant que sa destruction . La raison en est que, si vous êtes censé utiliser RTTI à de telles fins, vous créez des classes que L ignore. Cela signifie que votre code alloue l'objet et le transmet à L pour la gestion.

Maintenant, il y a des cas où quelque chose comme ça est une conception légitime. Signalisation d'événements/passage de messages, files d'attente de travail sécurisées pour les threads, etc. .

En C, ce modèle est orthographié void*, Et son utilisation nécessite beaucoup de soin pour éviter d'être cassé. En C++, ce modèle est orthographié std::experimental::any (bientôt orthographié std::any).

La façon dont cela devrait fonctionner est que L fournit une classe node_base Qui prend un any qui représente vos données réelles . Lorsque vous recevez le message, l'élément de travail de file d'attente de threads, ou quoi que vous fassiez, vous convertissez ensuite any en son type approprié, que l'expéditeur et le destinataire connaissent.

Ainsi, au lieu de dériver orange_node De node_data, Il vous suffit de coller un orange à l'intérieur du champ membre any de node_data. L'utilisateur final l'extrait et utilise any_cast Pour le convertir en orange. Si la conversion échoue, ce n'est pas orange.

Maintenant, si vous êtes familier avec l'implémentation de any, vous direz probablement: "Hé, attendez une minute: any en interne utilise RTTI pour faire fonctionner any_cast. " A quoi je réponds: "... oui".

C'est le point d'une abstraction . Au fond des détails, quelqu'un utilise RTTI. Mais au niveau où vous devriez opérer, le RTTI direct n'est pas quelque chose que vous devriez faire.

Vous devez utiliser des types qui vous fournissent les fonctionnalités souhaitées. Après tout, vous ne voulez pas vraiment RTTI. Ce que vous voulez, c'est une structure de données qui peut stocker une valeur d'un type donné, la cacher à tout le monde sauf la destination souhaitée, puis être reconvertie dans ce type, avec vérification que la valeur stockée est bien de ce type.

Cela s'appelle any. Il utilise RTTI, mais l'utilisation de any est de loin supérieure à l'utilisation directe de RTTI, car elle correspond mieux à la sémantique souhaitée.

18
Nicol Bolas

Si vous appelez une fonction, en règle générale, vous ne vous souciez pas vraiment des étapes précises qu'elle prendra, mais seulement qu'un objectif de niveau supérieur sera atteint dans certaines limites (et comment la fonction rend cela possible, c'est vraiment son propre problème).

Lorsque vous utilisez RTTI pour faire une présélection d'objets spéciaux qui peuvent faire un certain travail, alors que d'autres dans le même ensemble ne le peuvent pas, vous brisez cette vision confortable du monde. Tout à coup, l'appelant est censé savoir qui peut faire quoi, au lieu de simplement dire à ses serviteurs de continuer. Certaines personnes sont gênées par cela, et je soupçonne que c'est en grande partie la raison pour laquelle RTTI est considéré comme un peu sale.

Y a-t-il un problème de performances? Peut-être, mais je ne l'ai jamais expérimenté, et cela pourrait être la sagesse d'il y a vingt ans, ou celle de personnes qui croient honnêtement que l'utilisation de trois instructions de l'Assemblée au lieu de deux est un fardeau inacceptable.

Alors, comment y faire face ... Selon votre situation, il peut être judicieux de regrouper toutes les propriétés spécifiques aux nœuds dans des objets séparés (c'est-à-dire que l'API `` orange '' entière pourrait être un objet distinct). L'objet racine pourrait alors avoir une fonction virtuelle pour renvoyer l'API 'orange', renvoyant nullptr par défaut pour les objets non orange.

Bien que cela puisse être excessif en fonction de votre situation, cela vous permettrait de demander au niveau racine si un nœud spécifique prend en charge une API spécifique et, si c'est le cas, d'exécuter des fonctions spécifiques à cette API.

10
H. Guijt

C++ est construit sur l'idée d'une vérification de type statique.

[1]RTTI, c'est-à-dire dynamic_cast et type_id, est une vérification de type dynamique.

Donc, vous demandez essentiellement pourquoi la vérification de type statique est préférable à la vérification de type dynamique. Et la réponse simple est, si la vérification de type statique est préférable à la vérification de type dynamique, cela dépend. Sur beaucoup. Mais C++ est l'un des langages de programmation conçus autour de l'idée de vérification de type statique. Et cela signifie que par exemple le processus de développement, en particulier les tests, est généralement adapté à la vérification de type statique, puis correspond le mieux à cela.


Je ne connais pas une manière propre de faire cela avec des modèles ou d'autres méthodes

vous pouvez faire ce processus-nœuds-hétérogènes-d'un-graphique avec une vérification de type statique et sans aucun casting via le modèle de visiteur, par exemple comme ça:

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

Cela suppose que l'ensemble des types de nœuds est connu.

Quand ce n'est pas le cas, le modèle de visiteur (il existe de nombreuses variantes) peut être exprimé avec quelques modèles centralisés ou juste un seul.


Pour une approche basée sur un modèle, voir la bibliothèque Boost Graph. Malheureusement, je ne le connais pas, je ne l'ai pas utilisé. Je ne sais donc pas exactement ce qu'il fait et comment, et dans quelle mesure il utilise la vérification de type statique au lieu de RTTI, mais comme Boost est généralement basé sur un modèle avec la vérification de type statique comme idée centrale, je pense que vous constaterez que sa sous-bibliothèque Graph est également basée sur une vérification de type statique.


[1]  Informations sur le type d'exécution .

9

Bien sûr, il y a un scénario où le polymorphisme ne peut pas aider: les noms. typeid vous permet d'accéder au nom du type, bien que la façon dont ce nom est codé soit définie par l'implémentation. Mais ce n'est généralement pas un problème car vous pouvez comparer deux typeid- s:

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

Il en va de même pour les hachages.

[...] RTTI est "considéré comme nuisible"

nuisible est certainement exagéré: RTTI a quelques inconvénients, mais il a avantages aussi.

Vous n'avez pas vraiment besoin d'utiliser RTTI. RTTI est un outil pour résoudre OOP problèmes: si vous utilisez un autre paradigme, ceux-ci disparaîtront probablement. C n'a pas RTTI, mais fonctionne toujours. C++ à la place complètement prend en charge OOP et vous offre plusieurs outils pour surmonter certains problèmes pouvant nécessiter des informations d'exécution: l'un d'eux est en effet RTTI, qui a cependant un prix. Si vous ne pouvez pas vous le permettre, chose que vous feriez mieux de déclarer seulement après une analyse de performance sécurisée, il y a toujours la vieille école void*: c'est gratuit. Sans frais. Mais vous n'obtenez aucune sécurité de type. Il s'agit donc de métiers.


  • Certains compilateurs n'utilisent pas/RTTI n'est pas toujours activé
    Je n'achète vraiment pas cet argument. C'est comme dire que je ne devrais pas utiliser les fonctionnalités C++ 14, car il existe des compilateurs qui ne le prennent pas en charge. Et pourtant, personne ne me découragerait d'utiliser les fonctionnalités C++ 14.

Si vous écrivez (éventuellement strictement) du code C++ conforme, vous pouvez vous attendre au même comportement quelle que soit l'implémentation. Les implémentations conformes aux normes doivent prendre en charge les fonctionnalités C++ standard.

Mais considérez que dans certains environnements définis par C++ (ceux "autonomes"), RTTI n'a pas besoin d'être fourni, pas plus que les exceptions, virtual et ainsi de suite. RTTI a besoin d'une couche sous-jacente pour fonctionner correctement qui traite les détails de bas niveau tels que l'ABI et les informations de type réelles.


Je suis d'accord avec Yakk concernant RTTI dans ce cas. Oui, il pourrait être utilisé; mais est-ce logiquement correct? Le fait que la langue vous permette de contourner cette vérification ne signifie pas que cela devrait être fait.

3
edmz