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?
c++
object-oriented
object-oriented-design
Matthew James Briggs
la source
la source
Réponses:
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ôtstd::vector
des 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
QObject
classe 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).la source
Object
._hashCode
n'est pas "utiliser un autre conteneur" mais Le fait que C ++std::unordered_map
utilise 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é.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.
la source
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.
la source
cout.print(x).print(0.5).print("Bye\n")
- elle ne dépend pasoperator<<
.Parce que:
La mise en œuvre une sorte de
virtual
fonction 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
toString
virtuelle 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
equals
ouhashCode
ne 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, unint
vraiment ne devrait pas avoir une identité autre que sa valeur ... deux entiers de même valeur devraient être égaux.)la source
open
mot clé. Je ne pense cependant pas que cela a dépassé quelques papiers.shared_ptr<Foo>
pour voir si c'est aussi unshared_ptr<Bar>
(ou de même avec d'autres types de pointeur), même siFoo
etBar
sont 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.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 semblableboost::any
peut le faire sans une classe racine commune. Etboost::any
prend é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:
Maintenant, chaque morceau de code que vous écrivez peut être
serialization_friendly
.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 untuple
, ou unint
ou undouble
, ou unstd::ptrdiff_t
.Nous prenons une autre approche:
qui consiste à, eh bien, ne rien faire, apparemment. Sauf que maintenant nous pouvons étendre
write_to
en remplaçant enwrite_to
tant 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:
et nous pouvons maintenant prendre un type arbitraire et le mettre automatiquement dans une
can_serialize
interface qui vous permet d’invoquerserialize
ultérieurement via une interface virtuelle.Alors:
est une fonction qui prend tout ce qui peut sérialiser, au lieu de
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 écrasantwrite_to( my_buffer*, std::vector<T> const& )
- avec cette surcharge, il peut être passé à uncan_serialize
et la sérialisabilité desstd::vector
objets 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.
la source
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.
la source
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 queCObject
c’était la classe de base de presque toutes les classes (malheureusement pas d’CString
équivalentstring
dans 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.
la source
TObject
avant 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.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::vector
ici car il ne met pas en œuvreIObject
- je ferais mieux de créer un nouveau dérivéIVectorObject
qui 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
Integer
et laint
confusion 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.)la source
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 pouvezstatic_cast
revenir à la classe d'origine.Cependant, ce que vous ne pouvez pas faire avec
void
ce que vous pouvez faireObject
est 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.la source
[[no_unique_address]]
, qui peut être utilisé par les compilateurs pour donner une largeur nulle aux sous-objets membres.[[no_unique_address]]
autorisera le compilateur à utiliser les variables membres EBO.Java adopte la philosophie de conception selon laquelle le comportement non défini ne doit pas exister . Code tel que:
va tester si
felix
détient un sous-type deCat
celui qui implémente l'interfaceWoofer
; Si tel est le cas, il exécutera la conversion et l'invocationwoof()
, sinon une exception sera lancée. Le comportement du code est entièrement défini, qu'il soitfelix
implémentéWoofer
ou 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
*Cat
en*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.la source
Woofer
est une interface etCat
est héritable, le casting sera légitime car il pourrait exister (sinon maintenant, éventuellement à l'avenir) unWoofingCat
qui hériteCat
et est implémentéWoofer
. Notez que sous le modèle de compilation / liaison en Java, la création d'unWoofingCat
ne nécessiterait pas l'accès au code source deCat
niWoofer
.Cat
à aWoofer
et 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.dynamic_cast
aura 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 ...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.
la source