Pourquoi est-on découragé de base-pour-tout-objets en C ++

76

Stroustrup a déclaré: "N'inventez pas immédiatement une base unique pour toutes vos classes (une classe Object). Généralement, vous pouvez faire mieux sans cette classe pour beaucoup / la plupart des classes." (Langage de programmation C ++, quatrième édition, sect. 1.3.4)

Pourquoi une classe de base pour tout est-elle généralement une mauvaise idée et quand est-il judicieux d'en créer une?

Matthew James Briggs
la source
16
parce que C ++ n'est pas Java ... Et vous ne devriez pas essayer de le forcer.
AK_
10
Interrogé sur le dépassement de pile: Pourquoi n'y a-t-il pas de classe de base en C ++?
26
De plus, je ne suis pas d'accord avec les votes serrés pour "principalement fondée sur l'opinion." Cela peut s'expliquer par des raisons très spécifiques, comme l'attestent les réponses à la fois à cette question et à la question liée à la SO.
2
C’est le principe de l’agilité «vous n’en aurez pas besoin». À moins d’avoir déjà identifié un besoin particulier, ne le faites pas (jusqu’à ce que vous le fassiez).
Jool
3
@AK_: Il manque un "aussi stupide que" dans votre commentaire.
DeadMG

Réponses:

75

Car qu'est-ce que cet objet aurait pour fonctionnalité? En Java, toute la classe de base a une variable toString, un hashCode & equal et une variable monitor + condition.

  • ToString n'est utile que pour le débogage.

  • hashCode n'est utile que si vous souhaitez le stocker dans une collection basée sur le hachage (la préférence en C ++ est de transmettre une fonction de hachage au conteneur en tant que paramètre template ou d'éviter std::unordered_*complètement et d'utiliser plutôt std::vectordes listes non ordonnées).

  • l’égalité sans objet de base peut être aidée au moment de la compilation; s’ils n’ont pas le même type, ils ne peuvent pas être égaux. En C ++, il s'agit d'une erreur de compilation.

  • la variable de surveillance et de condition est mieux explicitement incluse au cas par cas.

Cependant, lorsqu'il y a plus à faire, il y a un cas d'utilisation.

Par exemple, dans QT, il y a la QObjectclasse racine qui constitue la base de l'affinité du fil, de la hiérarchie de propriété parent-enfant et du mécanisme des intervalles de signal. Cela force également l'utilisation par pointeur pour QObjects, cependant de nombreuses classes dans Qt n'héritent pas de QObject car elles n'ont pas besoin du slot-signal (en particulier les types de valeur de certaines descriptions).

monstre à cliquet
la source
7
Vous avez oublié de mentionner probablement la raison principale pour laquelle Java a une classe de base: avant les génériques, les classes de collection avaient besoin de la classe de base pour fonctionner. Tout (stockage interne, paramètres, valeurs de retour) a été saisi Object.
Aleksandr Dubinsky
1
@AleksandrDubinsky: Et les génériques n'ont ajouté que du sucre syntaxique, ne changeant vraiment rien, mais le vernis.
Déduplicateur
4
Je dirais que le code de hachage, l'égalité et la prise en charge du moniteur sont également des erreurs de conception en Java. Qui a pensé que c'était une bonne idée de verrouiller tous les objets?!
usr
1
Oui, mais personne ne le veut. À quand remonte la dernière fois que vous avez eu besoin de verrouiller un objet et que vous ne pouviez pas instancier un objet de verrouillage distinct pour le faire? C'est très rare et cela alourdit tout. Les gars de Java avaient une mauvaise compréhension de la sécurité des threads à l’époque, comme le prouvent tous les objets qui étaient un verrou et les collections thread-safe qui sont maintenant obsolètes. La sécurité des threads est une propriété globale, pas une propriété par objet.
usr
2
" hashCode n’est utile que si vous souhaitez le stocker dans une collection basée sur un hachage (la préférence en C ++ concerne les listes std :: vector et plain non ordored). " Le véritable contre-argument _hashCoden'est pas "utiliser un autre conteneur" mais Le fait que C ++ std::unordered_maputilise un argument de modèle au lieu d’exiger que la classe d’élément elle-même fournisse l’implémentation. C’est-à-dire que, comme tous les autres bons conteneurs et gestionnaires de ressources en C ++, il n’est pas intrusif; il ne pollue pas tous les objets contenant des fonctions ou des données au cas où quelqu'un en aurait besoin ultérieurement dans un contexte donné.
underscore_d
100

Parce qu'il n'y a pas de fonctions partagées par tous les objets. Il n'y a rien à mettre dans cette interface qui aurait du sens pour toutes les classes.

DeadMG
la source
10
+1 pour la simplicité de la réponse, c'est vraiment la seule raison.
BWG
7
Dans le cadre général que j'ai l'habitude d'utiliser, la classe de base commune fournit l'infrastructure de sérialisation et de réflexion souhaitée dans <tout> contexte. Meh Cela a simplement abouti à ce que les gens sérialisent un tas de fichiers avec les données et les métadonnées et rendent le format de données trop volumineux et complexe pour être efficace.
dmckee
19
@dmckee: Je dirais également que la sérialisation et la réflexion ne sont pas des besoins universellement utiles.
DeadMG
16
@DeadMG: "MAIS ET SI VOUS AVEZ BESOIN D'ÉCONOMISER TOUT?"
deworde
8
Je ne sais pas, vous le mettez entre guillemets, vous utilisez des majuscules et les gens ne peuvent pas voir la blague. @MSalters: Eh bien, cela devrait être facile, il y a un minimum d'état, vous spécifiez qu'il est là. Je peux écrire mon nom sur une liste sans entrer dans une boucle récursive.
deworde
25

Chaque fois que vous créez des hiérarchies élevées d'héritage d'objets, vous avez tendance à rencontrer le problème de la classe de base fragile (Wikipedia.) .

Le fait de disposer de nombreuses petites hiérarchies d'héritage distinctes (distinctes, isolées) réduit les risques de rencontrer ce problème.

L'intégration de tous vos objets dans une énorme hiérarchie d'héritage garantit pratiquement que vous allez rencontrer ce problème.

Mike Nakis
la source
6
Lorsque la classe de base ("java.lang.Object" en Java) ne contient aucune méthode qui appelle d'autres méthodes, le problème de la classe de base fragile ne peut pas se produire.
Martin Rosenau
3
Une puissante classe de base utile!
Mike Nakis
9
@MartinRosenau ... comme vous pouvez le faire en C ++ sans avoir besoin d'une classe de base maître!
gbjbaanb
5
@ DavorŽdralo Donc C ++ a un nom stupide pour une fonction de base ("opérateur <<" au lieu de quelque chose de sensé tel que "DebugPrint"), alors que Java a un fiasco d'une classe de base pour absolument chaque classe que vous écrivez, sans exception. Je pense que j'aime plus la verrue de C ++.
Sebastian Redl
4
@ DavorŽdralo: Le nom de la fonction est sans importance. Image une syntaxe cout.print(x).print(0.5).print("Bye\n")- elle ne dépend pas operator<<.
MSalters
24

Parce que:

  1. Vous ne devriez pas payer pour ce que vous n'utilisez pas.
  2. Ces fonctions ont moins de sens dans un système de types basé sur des valeurs que dans un système de types basé sur des références.

La mise en œuvre une sorte de virtualfonction présente une table virtuelle, qui nécessite par objet espace supérieur qui est ni nécessaire , ni souhaité dans de nombreuses situations ( la plupart?).

L'implémentation non toStringvirtuelle serait plutôt inutile, car la seule chose qu'il puisse renvoyer est l'adresse de l'objet, très peu conviviale pour l'utilisateur, et à laquelle l'appelant a déjà accès, contrairement à Java.
De même, un non-virtuel equalsou hashCodene peut utiliser que des adresses pour comparer des objets, ce qui est encore une fois plutôt inutile et souvent même carrément faux. Contrairement à Java, les objets sont copiés fréquemment en C ++, ce qui permet de distinguer "l'identité" d'un objet. toujours significatif ou utile. (Par exemple, un intvraiment ne devrait pas avoir une identité autre que sa valeur ... deux entiers de même valeur devraient être égaux.)

Mehrdad
la source
En ce qui concerne ce problème et le problème de la classe de base fragile évoqué par Mike Nakis, notez les recherches / propositions intéressantes pour le résoudre en Java, en rendant toutes les méthodes internes (c'est-à-dire appelées de la même classe) non virtuelles tout en conservant leur comportement virtuel lorsque appelé à l'extérieur; pour obtenir un comportement ancien / standard (c'est-à-dire virtuel partout), la proposition introduit un nouveau openmot clé. Je ne pense cependant pas que cela a dépassé quelques papiers.
Fizz
On trouvera un peu plus de détails sur ce document à l' adresse lambda-the-ultimate.org/classic/message12271.html
Fizz le
Avoir une classe de base commune permettrait de tester n'importe qui shared_ptr<Foo> pour voir si c'est aussi un shared_ptr<Bar>(ou de même avec d'autres types de pointeur), même si Fooet Barsont des classes non apparentées qui ne se connaissent pas. Exiger qu'une telle chose fonctionne avec des "pointeurs bruts", étant donné l'historique de leur utilisation, coûterait cher, mais pour les choses qui vont être stockées en tas de toute façon, le coût supplémentaire serait minime.
Supercat
Bien qu'il ne soit peut-être pas utile de disposer d'une classe de base commune pour tout, je pense qu'il existe certaines catégories assez grandes d'objets pour lesquelles des classes de base communes seraient utiles. Par exemple, de nombreuses classes (une majorité substantielle, sinon la majorité) en Java peuvent être utilisées de deux manières: en tant que détenteur non partagé de données mutables, ou en tant que détenteur partageable de données que personne ne peut modifier. Avec les deux modèles d'utilisation, un pointeur géré (référence) est utilisé comme proxy pour les données sous-jacentes. Pouvoir avoir un type de pointeur géré commun pour toutes ces données est utile.
Supercat
16

Avoir un seul objet racine limite ce que vous pouvez faire et ce que le compilateur peut faire, sans grand profit.

Une classe racine commune permet de créer des conteneurs-de-n'importe quoi et d'extraire ce qu'ils sont avec un dynamic_cast, mais si vous avez besoin de conteneurs-de-n'importe quoi alors quelque chose de semblable boost::anypeut le faire sans une classe racine commune. Et boost::anyprend également en charge les primitives - il peut même prendre en charge l’optimisation de la mémoire tampon réduite et les laisser presque "décompressées" dans le jargon Java.

C ++ prend en charge et prospère sur les types de valeur. Les deux littéraux et les types de valeur écrits par le programmeur. Les conteneurs C ++ stockent, trient, hachent, consomment et produisent efficacement des types de valeur.

L'héritage, en particulier le type d'héritage monolithique qu'impliquent les classes de base de style Java, nécessite des types "pointeur" ou "référence" basés sur des magasins gratuits. Votre handle / pointeur / référence à data contient un pointeur sur l'interface de la classe, et polymorphically pourrait représenter autre chose.

Bien que cela soit utile dans certaines situations, une fois que vous vous êtes marié au modèle avec une "classe de base commune", vous avez verrouillé l'intégralité de votre base de code sur le coût et le bagage de ce modèle, même lorsque cela n'était pas utile.

Presque toujours, vous en savez plus sur un type que "c'est un objet" sur le site appelant ou dans le code qui l'utilise.

Si la fonction est simple, l'écrire en tant que modèle vous donne un polymorphisme basé sur le temps de compilation du type de canard où les informations sur le site appelant ne sont pas rejetées. Si la fonction est plus complexe, l’effacement des types peut être effectué de sorte que les opérations uniformes sur le type que vous voulez effectuer (par exemple, la sérialisation et la désérialisation) puissent être générées et stockées (au moment de la compilation) pour être consommées (au moment de l’exécution) par le code dans une unité de traduction différente.

Supposons que vous ayez une bibliothèque où vous voulez que tout soit sérialisable. Une approche consiste à avoir une classe de base:

struct serialization_friendly {
  virtual void write_to( my_buffer* ) const = 0;
  virtual void read_from( my_buffer const* ) = 0;
  virtual ~serialization_friendly() {}
};

Maintenant, chaque morceau de code que vous écrivez peut être serialization_friendly.

void serialize( my_buffer* b, serialization_friendly const* x ) {
  if (x) x->write_to(b);
}

Sauf que pas un std::vector, alors maintenant vous devez écrire chaque conteneur. Et pas les entiers que vous avez obtenus de cette bibliothèque bignum. Et pas ce type, vous avez écrit que vous ne pensiez pas besoin de sérialisation. Et pas un tuple, ou un intou un double, ou un std::ptrdiff_t.

Nous prenons une autre approche:

void write_to( my_buffer* b, int x ) {
  b->write_integer(x);
}    
template<class T,
  class=std::enable_if_t< void_t<
    std::declval<T const*>()->write_to( std::declval<my_buffer*>()
  > >
>
void write_to( my_buffer* b, T const* x ) {
  if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
  write_to( b, t );
}

qui consiste à, eh bien, ne rien faire, apparemment. Sauf que maintenant nous pouvons étendre write_toen remplaçant en write_totant que fonction libre dans l'espace de nommage d'un type ou une méthode dans le type.

On peut même écrire un peu de code d'effacement de type:

namespace details {
  struct can_serialize_pimpl {
    virtual void write_to( my_buffer* ) const = 0;
    virtual void read_from( my_buffer const* ) = 0;
    virtual ~can_serialize_pimpl() {}
  };
}
struct can_serialize {
  void write_to( my_buffer* b ) const { pImpl->write_to(b); }
  void read_from( my_buffer const* b ) { pImpl->read_from(b); }
  std::unique_ptr<details::can_serialize_pimpl> pImpl;
  template<class T> can_serialize(T&&);
};
namespace details { 
  template<class T>
  struct can_serialize : can_serialize_pimpl {
    std::decay_t<T>* t;
    void write_to( my_buffer*b ) const final override {
      serialize( b, std::forward<T>(*t) );
    }
    void read_from( my_buffer const* ) final override {
      deserialize( b, std::forward<T>(*t) );
    }
    can_serialize(T&& in):t(&in) {}
  };
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
  std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}

et nous pouvons maintenant prendre un type arbitraire et le mettre automatiquement dans une can_serializeinterface qui vous permet d’invoquer serializeultérieurement via une interface virtuelle.

Alors:

void writer_thingy( can_serialize s );

est une fonction qui prend tout ce qui peut sérialiser, au lieu de

void writer_thingy( serialization_friendly const* s );

et le premier, contrairement au second, il peut gérer int, std::vector<std::vector<Bob>>automatiquement.

Cela n'a pas pris beaucoup de temps pour l'écrire, en particulier parce que ce genre de chose est quelque chose que vous ne voulez que rarement, mais nous avons gagné la possibilité de traiter n'importe quoi comme sérialisable sans nécessiter de type de base.

Quoi de plus, nous pouvons maintenant rendre std::vector<T>sérialisable en tant que citoyen de premier ordre simplement en écrasant write_to( my_buffer*, std::vector<T> const& )- avec cette surcharge, il peut être passé à un can_serializeet la sérialisabilité des std::vectorobjets est stockée dans une table virtuelle et accessible par .write_to.

En bref, le C ++ est suffisamment puissant pour que vous puissiez implémenter les avantages d’une classe de base unique à la volée, sans avoir à payer le prix d’une hiérarchie à héritage forcé s’il n’est pas requis. Et les moments où la seule base (truquée ou non) est requise sont relativement rares.

Lorsque les types sont réellement leur identité et que vous savez ce qu’ils sont, les possibilités d’optimisation abondent. Les données sont stockées localement et de manière contiguë (ce qui est très important pour la convivialité du cache sur les processeurs modernes), les compilateurs peuvent facilement comprendre le fonctionnement d’une opération donnée de l’autre côté), ce qui permet de réorganiser les instructions de manière optimale, et moins de chevilles rondes sont enfoncées dans des trous ronds.

Yakk
la source
8

Il y a beaucoup de bonnes réponses ci-dessus, et le fait évident que tout ce que vous feriez avec une classe de base de tous les objets peut être mieux fait autrement, comme le montre la réponse de @ ratchetfreak et les commentaires y relatifs sont très importants, mais il y a une autre raison, qui est d'éviter de créer des diamants d'héritagequand l'héritage multiple est utilisé. Si vous avez des fonctionnalités dans une classe de base universelle, dès que vous commencerez à utiliser l'héritage multiple, vous devrez commencer par spécifier la variante de celle-ci à laquelle vous souhaitez accéder, car elle pourrait être surchargée différemment dans différents chemins de la chaîne d'héritage. Et la base ne peut pas être virtuelle, car cela serait très inefficace (obliger tous les objets à disposer d’une table virtuelle à un coût potentiellement énorme en termes d’utilisation de la mémoire et de la localité). Cela deviendrait très rapidement un cauchemar logistique.

Jules
la source
1
Une solution au problème de diamant consiste à faire en sorte que tous les types dont un type de base non virtuel dérive via plusieurs chemins remplacent tous les membres virtuels de ce type de base; si un type de base commun avait été intégré dans le langage dès le départ, un compilateur pourrait générer automatiquement des implémentations par défaut légitimes (mais pas nécessairement impressionnantes).
Supercat
5

En fait, les premiers compilateurs et bibliothèques C ++ de Microsofts (je connais Visual C ++, 16 bits) avaient une telle classe nommée CObject.

Cependant, vous devez savoir qu’à ce moment-là, les "modèles" n’étaient pas supportés par ce compilateur C ++ simple, ce qui rendait des classes similaires std::vector<class T>. Au lieu de cela, une implémentation "vectorielle" ne pouvait gérer qu'un seul type de classe, il y avait donc une classe comparable à celle d' std::vector<CObject>aujourd'hui. Parce que CObjectc’était la classe de base de presque toutes les classes (malheureusement pas d’ CStringéquivalent stringdans les compilateurs modernes), vous pouvez utiliser cette classe pour stocker presque tous les types d’objets.

Comme les compilateurs modernes prennent en charge les modèles, ce cas d'utilisation d'une "classe de base générique" n'est plus fourni.

Vous devez penser au fait que l'utilisation d'une telle classe de base générique coûtera (un peu) en mémoire et en temps d'exécution - par exemple lors de l'appel au constructeur. Il y a donc des inconvénients lors de l'utilisation d'une telle classe, mais au moins lors de l'utilisation de compilateurs C ++ modernes, il n'y a pratiquement aucun cas d'utilisation pour une telle classe.

Martin Rosenau
la source
3
Est-ce que c'est MFC? [commentaire padding]
user253751
3
C'est en effet MFC. Un phare brillant de la conception OO qui a montré au monde entier comment faire les choses. Oh, attendez ...
gbjbaanb
4
@gbjbaanb Turbo Pascal et Turbo C ++ avaient leur propre TObjectavant même que MFC n'existe. Ne blâmez pas Microsoft pour cette partie de la conception, cela semblait être une bonne idée pour à peu près tout le monde à cette époque.
hév
Même avant les modèles, essayer d'écrire Smalltalk en C ++ produisait des résultats épouvantables.
JDługosz
@hvd Malgré tout, MFC était un exemple bien pire de conception orientée objet que tout ce que Borland avait produit.
Jules
5

Je vais suggérer une autre raison qui vient de Java.

Parce que vous ne pouvez pas créer une classe de base pour tout, du moins pas sans un tas de plaques chauffantes.

Vous pourrez peut-être vous en sortir pour vos propres cours, mais vous constaterez probablement que vous finissez par dupliquer beaucoup de code. Par exemple, "je ne peux pas utiliser std::vectorici car il ne met pas en œuvre IObject- je ferais mieux de créer un nouveau dérivé IVectorObjectqui fait la bonne chose ...".

Ce sera le cas chaque fois que vous utiliserez des classes de bibliothèques intégrées ou standard ou des classes d'autres bibliothèques.

Maintenant, si cela était intégré dans le langage, vous vous retrouveriez avec des éléments tels que le Integeret la intconfusion qui existe en Java, ou un changement important de la syntaxe du langage. (Remarquez que je pense que d'autres langues ont bien réussi à l'intégrer dans tous les types - le rubis semble être un meilleur exemple.)

Notez également que si votre classe de base n’est pas polymorphe au moment de l’exécution (c’est-à-dire avec des fonctions virtuelles), vous pourriez tirer le même avantage de l’utilisation de traits comme framework.

Par exemple, au lieu de .toString()vous, vous pourriez avoir les éléments suivants: (NOTE: je sais que vous pouvez le faire plus facilement en utilisant des bibliothèques existantes, etc., c’est juste un exemple illustratif.)

template<typename T>
struct ToStringTrait;

template<typename T> 
std::string toString(const T & t) {
  return ToStringTrait<T>::toString(t);
}

template<>
struct ToStringTrait<int> {
  std::string toString(int v) {
    return itoa(v);
  }
}

template<typename T>
struct ToStringTrait<std::vector<T>> {
  std::string toString(const std::vector<T> &v) {
    std::stringstream ss;
    ss<<"{";
    for(int i=0; i<v.size(); ++i) {
      ss<<toString(v[i]);
    }
    ss<<"}";
    return ss.str();
  }
}
Michael Anderson
la source
3

On peut dire que "vide" remplit beaucoup des rôles d'une classe de base universelle. Vous pouvez convertir n'importe quel pointeur en a void*. Vous pouvez ensuite comparer ces pointeurs. Vous pouvez static_castrevenir à la classe d'origine.

Cependant, ce que vous ne pouvez pas faire avec voidce que vous pouvez faire Objectest d’utiliser RTTI pour déterminer le type d’objet que vous possédez réellement. Cela tient finalement au fait que tous les objets en C ++ ne disposent pas de RTTI, et il est en effet possible d’avoir des objets de largeur nulle.

pjc50
la source
1
Seuls les sous-objets de classe de base de largeur zéro, pas les normaux.
Déduplicateur
@DuDuplicator À titre de mise à jour, C ++ 17 ajoute [[no_unique_address]], qui peut être utilisé par les compilateurs pour donner une largeur nulle aux sous-objets membres.
underscore_d
1
@underscore_d Vous voulez dire prévu pour C ++ 20, [[no_unique_address]]autorisera le compilateur à utiliser les variables membres EBO.
Déduplicateur
@ Déduplicateur Whoops, yup. J'ai déjà commencé à utiliser le C ++ 17, mais je pense toujours que c'est plus sophistiqué qu'il ne l'est réellement!
underscore_d
2

Java adopte la philosophie de conception selon laquelle le comportement non défini ne doit pas exister . Code tel que:

Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();

va tester si felixdétient un sous-type de Catcelui qui implémente l'interface Woofer; Si tel est le cas, il exécutera la conversion et l'invocation woof(), sinon une exception sera lancée. Le comportement du code est entièrement défini, qu'il soit feliximplémenté Wooferou non .

C ++ adopte la philosophie suivante: si un programme ne tente pas d'opération, le code généré ne devrait pas avoir d'impact si l'opération était tentée, et l'ordinateur ne devait pas perdre de temps à essayer de contraindre le comportement dans les cas où "devrait" ne se pose jamais. En C ++, en ajoutant les opérateurs d'indirection appropriés afin de convertir un *Caten *Woofer, le code produirait un comportement défini lorsque la conversion est légitime, mais un comportement non défini lorsqu'il ne l'est pas .

Avoir un type de base commun permet de valider les conversions entre des dérivés de ce type et d'effectuer des opérations d'essai, mais la validation des transmissions est plus coûteuse que de supposer qu'elles sont légitimes et que rien ne se passe mal. La philosophie C ++ est qu'une telle validation nécessite "de payer pour quelque chose dont vous n'avez [habituellement] pas besoin".

Un autre problème lié au C ++, mais qui ne le serait pas pour un nouveau langage, est que si plusieurs programmeurs créent chacun une base commune, en tirent leurs propres classes et écrivent du code pour travailler avec des éléments de cette classe de base commune, Un tel code ne pourra pas fonctionner avec des objets développés par des programmeurs utilisant une classe de base différente. Si un nouveau langage requiert que tous les objets de tas aient un format d'en-tête commun et n'a jamais autorisé les objets de tas non autorisés, une méthode qui nécessite une référence à un objet de tas avec un tel en-tête acceptera une référence à n'importe quel objet de tas pourrait jamais créer.

Personnellement, je pense qu'avoir un moyen commun de demander à un objet "es-tu convertible au type X" est-il une fonctionnalité très importante dans un langage / framework, mais si une telle fonctionnalité n'est pas intégrée dans un langage dès le départ, il est difficile de ajoutez-le plus tard. Personnellement, je pense qu'une telle classe de base devrait être ajoutée à une bibliothèque standard à la première occasion, avec une forte recommandation selon laquelle tous les objets qui seront utilisés de manière polymorphe devraient hériter de cette base. Si les programmeurs implémentaient chacun leur propre "type de base", il serait plus difficile de passer des objets entre les codes de différentes personnes, mais un type de base commun dont de nombreux programmeurs ont hérité faciliterait la tâche.

ADDENDA

À l'aide de modèles, il est possible de définir un "détenteur d'objet arbitraire" et de lui demander le type d'objet qu'il contient. le paquet Boost contient une telle chose appelée any. Ainsi, même si le C ++ n’a pas de type standard "référence vérifiable à tout", il est possible d’en créer un. Cela ne résout pas le problème susmentionné de ne pas avoir quelque chose dans le langage standard, à savoir une incompatibilité entre les implémentations de différents programmeurs, mais cela explique comment le C ++ passe sans avoir un type de base à partir duquel tout est dérivé: en permettant de créer quelque chose qui agit comme un.

supercat
la source
Cette conversion échoue lors de la compilation en C ++ , Java et C # .
milleniumbug
1
@milleniumbug: Si Wooferest une interface et Catest héritable, le casting sera légitime car il pourrait exister (sinon maintenant, éventuellement à l'avenir) un WoofingCatqui hérite Catet est implémenté Woofer. Notez que sous le modèle de compilation / liaison en Java, la création d'un WoofingCatne nécessiterait pas l'accès au code source de Catni Woofer.
Supercat
3
C ++ a dynamic_cast , qui gère correctement le transtypage de a Catà a Wooferet répondra à la question "Êtes-vous convertible au type X". C ++ vous permettra de forcer un casting, parce que peut-être que vous savez ce que vous faites, mais cela vous aidera également si ce n'est pas ce que vous voulez vraiment faire.
Rob K
2
@RobK: Vous avez raison sur la syntaxe bien sûr; mea culpa. J'ai lu un peu plus sur dynamic_cast et il semble que, dans un sens, le C ++ moderne ait tous les objets polymorphes dérivés d'une classe de base "objet polymorphe" avec le ou les champs nécessaires pour identifier le type de l'objet (généralement une table vtable). pointeur, bien que ce soit un détail de mise en œuvre). C ++ ne décrit pas les classes polymorphes de cette façon, mais passer un pointeur sur dynamic_castaura un comportement défini s'il pointe vers un objet polymorphe, et un comportement indéfini s'il ne le fait pas, donc d'un point de vue sémantique ...
Supercat
2
... tous les objets polymorphes stockent certaines informations avec la même présentation et prennent en charge un comportement qui n'est pas pris en charge par les objets non polymorphes; Pour moi, cela signifie qu'ils se comportent comme s'ils provenaient d'une base commune, que la définition du langage utilise une telle terminologie ou non.
Supercat
1

Symbian C ++ avait en fait une classe de base universelle, CBase, pour tous les objets qui se comportaient de manière particulière (principalement s’ils allaient en tas). Il a fourni un destructeur virtuel, mis à zéro la mémoire de la classe lors de la construction et masqué le constructeur de copie.

La logique derrière était que c'était un langage pour les systèmes embarqués et les compilateurs C ++ et les spécifications étaient vraiment merdiques il y a 10 ans.

Toutes les classes n’en ont pas hérité, seulement certaines.

James
la source