Pourquoi n'y a-t-il pas de classe de base en C ++?

84

Question rapide: du point de vue de la conception, pourquoi est-ce que, en C ++, il n'y a pas de classe de base mère de tous, qu'est-ce qui est généralement objectdans d'autres langages?

slezica
la source
27
C'est vraiment plus une question philosophique qu'une question de conception. La réponse courte est "parce que Bjarne ne voulait apparemment pas faire cela."
Jerry Coffin
4
Rien ne vous empêche non plus d'en faire un.
GWW
22
Personnellement, je pense que c'est un défaut de conception que tout dérive de la même classe de base. Cela promet un style de programmation qui pose plus de problèmes qu'il n'en vaut la peine (car vous avez tendance à avoir des conteneurs d'objets génériques dont vous avez ensuite besoin pour utiliser l'objet réel (ce que je fronce les sourcils comme une mauvaise conception à mon humble avis)). Est-ce que je veux vraiment un conteneur pouvant abriter à la fois des voitures et des objets d'automatisation de commande?
Martin York
3
@Martin mais regardez les choses de cette façon: par exemple, jusqu'à C ++ 0x auto, vous deviez utiliser des définitions de type d'un kilomètre de long pour les itérateurs, ou des typedefs ponctuels . Avec une hiérarchie de classes plus générique, vous pouvez simplement utiliser objectou iterator.
slezica
9
@Santiago: L'utilisation d'un système de type unifié signifie presque toujours que vous vous retrouvez avec beaucoup de code qui repose sur le polymorphisme, la répartition dynamique et le RTTI, qui sont tous relativement coûteux et qui empêchent tous les optimisations possibles lorsqu'elles ne sont pas utilisés.
James McNellis

Réponses:

116

La décision définitive se trouve dans la FAQ de Stroustrup . Bref, cela ne transmet aucune signification sémantique. Cela aura un coût. Les modèles sont plus utiles pour les conteneurs.

Pourquoi le C ++ n'a-t-il pas d'objet de classe universelle?

  • Nous n'en avons pas besoin: la programmation générique fournit des alternatives sûres de type statique dans la plupart des cas. D'autres cas sont traités à l'aide de l'héritage multiple.

  • Il n'y a pas de classe universelle utile: un vraiment universel n'a pas de sémantique propre.

  • Une classe "universelle" encourage une réflexion bâclée sur les types et les interfaces et conduit à une vérification excessive du temps d'exécution.

  • L'utilisation d'une classe de base universelle implique un coût: les objets doivent être alloués en tas pour être polymorphes; cela implique un coût de mémoire et d'accès. Les objets de tas ne prennent naturellement pas en charge la sémantique de copie. Les objets de tas ne prennent pas en charge le comportement de portée simple (ce qui complique la gestion des ressources). Une classe de base universelle encourage l'utilisation de dynamic_cast et d'autres vérifications au moment de l'exécution.

Capitaine Girafe
la source
83
Nous n'avons pas besoin d'objet de classe de base / / nous n'avons pas besoin de contrôle / pensée ...;)
Piskvor a quitté le bâtiment le
3
@Piskvor - LOL Je veux voir le clip vidéo!
Byron Whitlock
10
Objects must be heap-allocated to be polymorphic- Je ne pense pas que cette déclaration soit généralement correcte. Vous pouvez certainement créer une instance d'une classe polymorphe sur la pile et la transmettre en tant que pointeur vers l'une de ses classes de base, ce qui donne un comportement polymorphe.
Byron
5
En Java, tenter de Fooconvertir une référence-à-en-référence-à- Barquand Barn'hérite pas Foo, lève de manière déterministe une exception. En C ++, tenter de convertir un pointeur vers Foo en un pointeur vers barre pourrait renvoyer un robot dans le temps pour tuer Sarah Connor.
supercat
3
@supercat C'est pourquoi nous utilisons dynamic_cast <>. Pour éviter SkyNet et les canapés Chesterfield apparaissant mystérieusement.
Captain Giraffe
44

Voyons d'abord pourquoi vous voudriez avoir une classe de base en premier lieu. Je peux penser à plusieurs raisons différentes:

  1. Pour prendre en charge des opérations génériques ou des collections qui fonctionneront sur des objets de tout type.
  2. Pour inclure diverses procédures communes à tous les objets (comme la gestion de la mémoire).
  3. Tout est un objet (pas de primitifs!). Certains langages (comme Objective-C) ne l'ont pas, ce qui rend les choses assez compliquées.

Ce sont les deux bonnes raisons pour lesquelles les langages de la marque Smalltalk, Ruby et Objective-C ont des classes de base (techniquement, Objective-C n'a pas vraiment de classe de base, mais à toutes fins utiles, il en a).

Pour # 1, le besoin d'une classe de base qui unifie tous les objets sous une seule interface est évité par l'inclusion de modèles dans C ++. Par exemple:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

n'est pas nécessaire, lorsque vous pouvez maintenir l'intégrité du type tout au long du polymorphisme paramétrique!

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

Pour le n ° 2, alors qu'en Objective-C, les procédures de gestion de la mémoire font partie de l'implémentation d'une classe et sont héritées de la classe de base, la gestion de la mémoire en C ++ est effectuée en utilisant la composition plutôt que l'héritage. Par exemple, vous pouvez définir un wrapper de pointeur intelligent qui effectuera le comptage de références sur des objets de tout type:

template <class T>
struct refcounted
{
  refcounted(T* object) : _object(object), _count(0) {}

  T* operator->() { return _object; }
  operator T*() { return _object; }

  void retain() { ++_count; }

  void release()
  {
    if (--_count == 0) { delete _object; }
  }

  private:
    T* _object;
    int _count;
};

Ensuite, au lieu d'appeler des méthodes sur l'objet lui-même, vous appelleriez des méthodes dans son wrapper. Cela permet non seulement une programmation plus générique: cela vous permet également de séparer les préoccupations (car idéalement, votre objet devrait être plus préoccupé par ce qu'il doit faire, que par la façon dont sa mémoire doit être gérée dans différentes situations).

Enfin, dans un langage qui a à la fois des primitives et des objets réels comme C ++, les avantages d'avoir une classe de base (une interface cohérente pour chaque valeur) sont perdus, car vous avez alors certaines valeurs qui ne peuvent pas se conformer à cette interface. Afin d'utiliser des primitives dans ce genre de situation, vous devez les transformer en objets (si votre compilateur ne le fait pas automatiquement). Cela crée beaucoup de complications.

Donc, la réponse courte à votre question: C ++ n'a pas de classe de base car, ayant un polymorphisme paramétrique via des modèles, il n'en a pas besoin.

Jonathan Sterling
la source
C'est bon! Heureux que vous l'ayez trouvé utile :)
Jonathan Sterling
2
Pour le point 3, je contre avec C #. C # a des primitives qui peuvent être encadrées en object( System.Object), mais elles n'ont pas besoin de l'être. Pour le compilateur, intet System.Int32sont des alias et peuvent être utilisés de manière interchangeable; le runtime gère la boxe en cas de besoin.
Cole Johnson
En C ++, il n'y a pas besoin de boxer. Avec les modèles, le compilateur génère le code correct au moment de la compilation.
Rob K
2
Veuillez noter que bien que cette (ancienne) réponse démontre un pointeur intelligent compté par référence, C ++ 11 a maintenant std::shared_ptrce qui devrait être utilisé à la place.
15

Le paradigme dominant pour les variables C ++ est le pass-by-value et non le pass-by-ref. Forcer tout à dériver d'une racine Objectferait de leur passage par valeur une erreur ipse facto.

(Parce qu'accepter un objet par valeur comme paramètre, le couperait par définition et enlèverait son âme).

Ceci n'est pas le bienvenu. C ++ vous fait réfléchir à savoir si vous vouliez une sémantique de valeur ou de référence, vous donnant le choix. C'est une chose importante dans le calcul des performances.

sehe
la source
1
Ce ne serait pas une erreur en soi. Les hiérarchies de types existent déjà, et elles utilisent assez bien le passage par valeur lorsque c'est approprié. Cependant, cela encouragerait davantage de stratégies basées sur des tas là où le passage par référence est plus approprié.
Dennis Zickefoose
À mon humble avis, un bon langage devrait avoir des types d'héritage distincts pour les situations où une classe dérivée peut légitimement être utilisée comme objet de classe de base en utilisant tranche par référence, celles où elle peut être transformée en une classe en utilisant tranche par valeur et ceux où ni l'un ni l'autre n'est légitime. La valeur d'avoir un type de base commun pour les choses qui peuvent être découpées serait limitée, mais beaucoup de choses ne peuvent pas être découpées et doivent être stockées indirectement; le coût supplémentaire de faire dériver de telles choses d'un commun Objectserait relativement faible.
supercat
6

Le problème est qu'il existe un tel type en C ++! Ça l'est void. :-) Tout pointeur peut être converti implicitement en toute sécurité void *, y compris les pointeurs vers les types de base, les classes sans table virtuelle et les classes avec table virtuelle.

Puisqu'il doit être compatible avec toutes ces catégories d'objets, voidil ne peut lui - même contenir de méthodes virtuelles. Sans fonctions virtuelles et RTTI, aucune information utile sur le type ne peut être obtenue void(elle correspond à TOUS les types, donc ne peut dire que des choses qui sont vraies pour TOUS les types), mais les fonctions virtuelles et RTTI rendraient les types simples très inefficaces et empêcheraient C ++ d'être un langage adapté à la programmation de bas niveau avec accès direct à la mémoire, etc.

Donc, il existe un tel type. Il fournit juste une interface très minimaliste (en fait, vide) en raison de la nature de bas niveau du langage. :-)

Ellioh
la source
Les types de pointeurs et les types d'objets ne sont pas les mêmes. Vous ne pouvez pas avoir d'objet de type void.
Kevin Panko
1
En fait, c'est ainsi que l'héritage en C ++ fonctionne généralement. L'une des choses les plus importantes qu'il fait est la compatibilité des types pointeur et référence: lorsqu'un pointeur vers une classe est requis, un pointeur vers une classe dérivée peut être donné. Et la possibilité de diffuser des pointeurs static_cast entre deux types est un signe qu'ils sont connectés dans une hiérarchie. De ce point de vue, void est définitivement une classe de base universelle en C ++.
Ellioh
Si vous déclarez une variable de type, voidelle ne se compilera pas et vous obtenez cette erreur . En Java, vous pourriez avoir une variable de type Objectet cela fonctionnerait. C'est la différence entre voidet un type «réel». Il est peut-être vrai que voidc'est le type de base pour tout, mais comme il ne fournit aucun constructeur, méthode ou champ, il n'y a aucun moyen de dire qu'il est là ou non. Cette affirmation ne peut être ni prouvée ni réfutée.
Kevin Panko
1
En Java, chaque variable est une référence. Voilà la différence. :-) (BTW, en C ++ il y a aussi des classes abstraites que vous ne pouvez pas utiliser pour déclarer une variable). Et dans ma réponse, j'ai expliqué pourquoi il n'est pas possible d'obtenir RTTI de void. Donc, théoriquement vide est toujours une classe de base pour tout. static_cast est une preuve, car il effectue des transtypages uniquement entre les types apparentés, et il peut être utilisé pour void. Mais vous avez raison, l'absence d'accès RTTI in void rend vraiment très différent des types de base dans les langages de niveau supérieur.
Ellioh
1
@Ellioh Après avoir consulté le standard C ++ 11 §3.9.1p9, je concède que voidc'est un type (et j'ai découvert une utilisation? : un peu intelligente de ), mais il est encore loin d'être une classe de base universelle. D'une part, c'est «un type incomplet qui ne peut pas être complété», d'autre part, il n'y a pas de void&type.
bcrist
-2

C ++ est un langage fortement typé. Pourtant, il est surprenant qu'il n'ait pas de type d'objet universel dans le contexte de la spécialisation des modèles.

Prenons, par exemple, le modèle

template <class T> class Hook;
template <class ReturnType, class ... ArgTypes>
class Hook<ReturnType (ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

qui peut être instancié comme

Hook<decltype(some_function)> ...;

Supposons maintenant que nous voulons la même chose pour une fonction particulière. Comme

template <auto fallback> class Hook;
template <auto fallback, class ReturnType, class ... ArgTypes>
class Hook<ReturnType fallback(ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

avec l'instanciation spécialisée

Hook<some_function> ...

Mais hélas, même si la classe T peut remplacer n'importe quel type (classe ou non) avant la spécialisation, il n'y a pas d'équivalent auto fallback(j'utilise cette syntaxe comme la syntaxe non-type générique la plus évidente dans ce contexte) qui pourrait remplacer tout argument de modèle non-type avant la spécialisation.

Donc, en général, ce modèle ne transfère pas des arguments de modèle de type à des arguments de modèle non-type.

Comme avec beaucoup de coins en langage C ++, la réponse est probablement "aucun membre du comité n'y a pensé".

user7339377
la source
Même si (quelque chose comme) votre ReturnType fallback(ArgTypes...)travail fonctionnait, ce serait une mauvaise conception. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...fait ce que vous voulez et signifie que les paramètres du modèle Hooksont de nature
Caleth
-4

C ++ a été initialement appelé "C avec classes". C'est une progression du langage C, contrairement à d'autres choses plus modernes comme C #. Et vous ne pouvez pas voir le C ++ comme un langage, mais comme une base de langages (Oui, je me souviens du livre de Scott Meyers, Effective C ++).

C lui-même est un mélange de langages, le langage de programmation C et son préprocesseur.

C ++ ajoute un autre mélange:

  • l'approche classe / objets

  • modèles

  • la STL

Personnellement, je n'aime pas certaines choses qui viennent directement du C au C ++. Un exemple est la fonctionnalité enum. La façon dont C # permet au développeur de l'utiliser est bien meilleure: il limite l'énumération dans sa propre portée, il a une propriété Count et il est facilement itérable.

Comme C ++ voulait être rétrocompatible avec C, le concepteur a été très permissif pour permettre au langage C d'entrer dans son ensemble en C ++ (il y a quelques différences subtiles, mais je ne me souviens de rien que vous pourriez faire en utilisant un compilateur C que vous ne pouvait pas utiliser un compilateur C ++).

sergiol
la source
"ne peut pas voir le C ++ comme un langage, mais comme une base de langages" ... voulez-vous vraiment expliquer et illustrer ce que cette déclaration bizarre mène? Les paradigmes multiples sont distincts des «langages multiples», bien qu'il soit juste de dire que le préprocesseur est disjoint du reste des étapes du compilateur - bon point. Re enums - ils peuvent trivialement être enveloppés dans une portée de classe si vous le souhaitez, et le langage entier manque d'introspection - un problème majeur, bien que dans ce cas il puisse être approché - voir le code benum de Boost vault. Votre argument concerne-t-il uniquement Q: C ++ n'a pas commencé avec une base universelle?
Tony Delroy
@Tony: Curieusement, le livre auquel j'ai fait référence, la seule chose qu'ils ne mentionnent même pas comme une autre langue est le préprocesseur, qui est le seul que vous considérez séparément. Je comprends votre point de vue, car le préprocesseur analyse d'abord sa propre entrée. Mais, avoir un mécanisme de création de modèles, c'est comme avoir un autre langage qui fait la généralisation pour vous. Vous appliquez une syntaxe de modèle et vous aurez le compilateur générant des fonctions pour chaque type unique que vous avez. Quand vous pouvez avoir un programme écrit en C et donner son entrée à un compilateur C ++, c'est deux langages en un: C et C avec des objets => C ++
sergiol
STL, il peut être considéré comme un add-on, mais il a des classes et des conteneurs très pratiques qui, à travers des modèles, étendent NATIVEMENT la puissance du C ++. Et les énumérations que je disais sont les énumérations NATIVES. Lorsque vous parlez de Boost, vous vous appuyez sur un code tiers qui n'est pas natif de la langue. En C #, je n'ai pas besoin d'inclure quoi que ce soit d'extern pour avoir une énumération itérable et auto-portée. En C ++, une astuce que j'utilise souvent est d'appeler le dernier élément d'une énumération - sans valeurs assignées, donc ils sont automatiquement démarrés à partir de zéro et incrémentés - quelque chose comme NUM_ITEMS, et cela me donne la limite supérieure.
sergiol
2
merci pour vos explications supplémentaires - je peux au moins voir d'où vous venez; J'ai entendu quelques personnes se plaindre qu'apprendre à utiliser des modèles était disjoint des autres programmes C ++, bien que ce ne soit certainement pas mon expérience. Je laisserai aux autres lecteurs le soin de faire ce qu'ils voudront des autres perspectives que vous présentez. À votre santé.
Tony Delroy
1
Je pense que le plus gros problème avec un type de base universel est qu'un tel type serait inutile s'il n'avait pas de méthodes virtuelles; C ++ ne voulait pas ajouter de surcharge pour les types de structure qui n'ont aucune méthode virtuelle, mais chaque objet de tout type qui a des méthodes virtuelles doit avoir, au minimum, un pointeur vers une table de répartition. Si les classes et les structures avaient habité des univers différents, il aurait été possible d'avoir un objet de classe universel, mais les classes et les structures sont fondamentalement la même chose en C ++.
supercat