web-dev-qa-db-fra.com

Pourquoi ne devrait-on pas dériver de la classe de chaînes c ++ std?

Je voulais poser une question sur un point spécifique soulevé dans Effective C++. 

Ça dit:

Un destructeur doit être rendu virtuel si une classe doit agir comme une classe polymorphe. Il ajoute que, puisque std::string n’a pas de destructeur virtuel, il ne faut jamais en dériver. De plus, std::string n'est même pas conçu pour être une classe de base, oubliez la classe de base polymorphe. 

Je ne comprends pas ce qui est spécifiquement requis dans une classe pour pouvoir être une classe de base (pas une classe polymorphe)?

Est-ce que la seule raison pour laquelle je ne devrais pas dériver de la classe std::string est-ce qu'elle n'a pas de destructeur virtuel? À des fins de réutilisation, une classe de base peut être définie et plusieurs classes dérivées peuvent en hériter. Alors, qu'est-ce qui fait que std::string n'est même pas éligible en tant que classe de base?

De même, s'il existe une classe de base purement définie à des fins de réutilisabilité et s'il existe de nombreux types dérivés, existe-t-il un moyen d'empêcher le client de faire Base* p = new Derived() car les classes ne sont pas destinées à être utilisées de manière polymorphe?

60

Je pense que cette déclaration reflète la confusion ici (c'est moi qui souligne):

Je ne comprends pas ce qui est spécifiquement requis dans une classe pour être éligible pour être une classe de base (pas une classe polymorphe)?

En C++ idiomatique, dériver d'une classe a deux utilisations:

  • private héritage, utilisé pour les mixins et la programmation orientée aspect à l'aide de modèles.
  • héritage public, utilisé pour situations polymorphes uniquement. EDIT: D'accord, je suppose que cela pourrait également être utilisé dans quelques scénarios de mixage - tels que boost::iterator_facade -, qui apparaissent lorsque le CRTP est utilisé.

Il n'y a absolument aucune raison de dériver publiquement une classe en C++ si vous n'essayez pas de faire quelque chose de polymorphe. La langue est livrée avec des fonctions gratuites en tant que fonction standard de la langue, et les fonctions gratuites sont ce que vous devriez utiliser ici.

Pensez-y de cette façon: voulez-vous vraiment forcer les clients de votre code à utiliser une classe de chaînes propriétaire simplement parce que vous voulez utiliser quelques méthodes? Contrairement à Java ou à C # (ou à la plupart des langages orientés objet similaires), lorsque vous dérivez une classe en C++, la plupart des utilisateurs de la classe de base ont besoin de connaître ce type de modification. En Java/C #, les classes sont généralement accessibles via des références similaires aux pointeurs de C++. Par conséquent, il existe un niveau d'indirection impliqué qui dissocie les clients de votre classe, ce qui vous permet de remplacer une classe dérivée sans que d'autres clients le sachent.

Cependant, en C++, les classes sont des types value, contrairement à la plupart des autres langages OO. Le moyen le plus simple de voir ceci est ce qu'on appelle le problème de découpage } _. Fondamentalement, considérez:

int StringToNumber(std::string copyMeByValue)
{
    std::istringstream converter(copyMeByValue);
    int result;
    if (converter >> result)
    {
        return result;
    }
    throw std::logic_error("That is not a number.");
}

Si vous transmettez votre propre chaîne à cette méthode, le constructeur de copie pour std::string sera appelé pour effectuer une copie, pas le constructeur de copie pour votre objet dérivé - quelle que soit la classe enfant de std::string qui est transmise. Cela peut entraîner des incohérences entre vos méthodes et tout élément lié à la chaîne. La fonction StringToNumber ne peut pas simplement prendre ce que votre objet dérivé est et le copier, tout simplement parce que votre objet dérivé a probablement une taille différente de celle de std::string - mais cette fonction a été compilée pour ne réserver que l'espace pour un std::string en stockage automatique. En Java et en C #, cela ne pose pas de problème, car seuls les types de référence, tels que le stockage automatique, sont impliqués et les références ont toujours la même taille. Pas si en C++.

Longue histoire courte - n'utilisez pas l'héritage pour utiliser des méthodes en C++. Ce n'est pas idiomatique et entraîne des problèmes avec la langue. Utilisez si possible des fonctions non amis, non membres, suivies de la composition. N'utilisez pas l'héritage, sauf si vous utilisez une métaprogrammation de modèle ou si vous souhaitez un comportement polymorphe. Pour plus d'informations, voir Effective C++ de Scott Meyers, élément 23: Préférer les fonctions non membres non amis aux fonctions membres.

EDIT: Voici un exemple plus complet illustrant le problème du découpage en tranches. Vous pouvez voir sa sortie sur codepad.org

#include <ostream>
#include <iomanip>

struct Base
{
    int aMemberForASize;
    Base() { std::cout << "Constructing a base." << std::endl; }
    Base(const Base&) { std::cout << "Copying a base." << std::endl; }
    ~Base() { std::cout << "Destroying a base." << std::endl; }
};

struct Derived : public Base
{
    int aMemberThatMakesMeBiggerThanBase;
    Derived() { std::cout << "Constructing a derived." << std::endl; }
    Derived(const Derived&) : Base() { std::cout << "Copying a derived." << std::endl; }
    ~Derived() { std::cout << "Destroying a derived." << std::endl; }
};

int SomeThirdPartyMethod(Base /* SomeBase */)
{
    return 42;
}

int main()
{
    Derived derivedObject;
    {
        //Scope to show the copy behavior of copying a derived.
        Derived aCopy(derivedObject);
    }
    SomeThirdPartyMethod(derivedObject);
}
55
Billy ONeal

Offrir le contrepasse aux conseils généraux (ce qui est valable en l'absence de problèmes de verbosité/de productivité particuliers évidents) ...

Scénario d'utilisation raisonnable

Il existe au moins un scénario dans lequel la dérivation publique à partir de bases sans destructeurs virtuels peut être une bonne décision:

  • vous souhaitez bénéficier des avantages en matière de sécurité de type et de lisibilité du code fournis par les types (classes) dédiés définis par l'utilisateur
  • une base existante est idéale pour stocker les données et permet des opérations de bas niveau que le code client voudrait également utiliser
  • vous voulez pouvoir réutiliser des fonctions supportant cette classe de base
  • vous comprenez que tout invariant supplémentaire dont vos données ont logiquement besoin ne peut être imposé que par un code qui accède explicitement aux données en tant que type dérivé, et en fonction de la mesure dans laquelle cela se produira "naturellement" dans votre conception, et dans quelle mesure vous pouvez faire confiance au client. Afin de comprendre et de coopérer avec les invariants d’idéal logique, vous voudrez peut-être que les fonctions des membres de la classe dérivée revérifient les attentes (et les jettent ou peu importe)
  • la classe dérivée ajoute des fonctions très pratiques spécifiques aux types, telles que les recherches personnalisées, le filtrage/modification de données, la transmission en continu, l'analyse statistique, les itérateurs (alternatifs)
  • le couplage du code client à la base est plus approprié que le couplage à la classe dérivée (étant donné que la base est stable ou que les modifications qui y sont apportées reflètent des améliorations apportées aux fonctionnalités, également au cœur de la classe dérivée)
    • en d'autres termes: vous voulez que la classe dérivée continue d'exposer la même API que la classe de base, même si cela signifie que le code client est obligé de changer, plutôt que de l'isoler d'une manière qui permette aux API de base et dérivées de se développer. de synchronisation
  • vous n'allez pas mélanger les pointeurs sur la base et les objets dérivés dans des parties du code responsables de leur suppression

Cela peut sembler assez restrictif, mais il existe de nombreux cas dans les programmes du monde réel correspondant à ce scénario.

Discussion de fond: mérites relatifs

La programmation concerne les compromis. Avant d'écrire un programme plus "correct" sur le plan conceptuel:

  • déterminez si cela nécessite une complexité et un code supplémentaires qui obscurcissent la logique réelle du programme, et sont donc davantage sujets aux erreurs en dépit de la gestion plus robuste d'un problème spécifique.
  • comparer les coûts pratiques à la probabilité et aux conséquences des problèmes, et
  • songez au "retour sur investissement" et à ce que vous pourriez faire de plus avec votre temps.

Si les problèmes potentiels impliquent l’utilisation d’objets que vous ne pouvez imaginer, essayez de donner une idée de leur accessibilité, étendue et nature de l’utilisation dans le programme, ou vous pouvez générer des erreurs de compilation pour utilisation dangereuse (par exemple, une assertion selon laquelle la taille de classe dérivée correspond à celle de la base, ce qui empêcherait l'ajout de nouveaux membres de données); toute autre chose peut donc être une sur-ingénierie prématurée. Prenez la victoire facile dans une conception et un code propres, intuitifs et concis.

Raisons pour envisager une dérivation sans destructeur virtuel

Supposons que vous ayez une classe D dérivée publiquement de B. Sans aucun effort, les opérations sur B sont possibles sur D (à l'exception de la construction, mais même s'il y a beaucoup de constructeurs, vous pouvez souvent fournir une redirection efficace en ayant un modèle pour par exemple, template <typename T1, typename T2> D(const T1& x1, const T2& t2) : B(t1, t2) { }. Meilleure solution généralisée dans les modèles variadiques C++ 0x.)

En outre, si B change, D expose par défaut ces modifications (rester synchronisé), mais il est peut-être nécessaire de passer en revue la fonctionnalité étendue introduite dans D pour voir si elle reste valide, et l'utilisation du client.

En reformulant ceci: il y a couplage explicite réduit entre la base et la classe dérivée, mais couplage accru entre la base et le client.

Ce n’est souvent pas ce que vous voulez, mais parfois c’est l’idéal et d’autres fois un problème (voir le paragraphe suivant). Les modifications apportées à la base forcent davantage de modifications de code client dans les emplacements répartis dans la base de code. Parfois, les personnes qui modifient la base n'ont même pas accès au code client pour l'examiner ou le mettre à jour en conséquence. Cela vaut parfois mieux: si vous, en tant que fournisseur de classe dérivée - "l'homme du milieu" - souhaitez que les modifications de la classe de base soient transmises aux clients, et que vous souhaitiez généralement que les clients puissent - parfois forcés - mettre à jour leur code lorsque La classe de base change sans que vous ayez besoin d'être constamment impliqué, alors la dérivation publique peut être idéale. Ceci est courant lorsque votre classe n’est pas vraiment une entité indépendante, mais une légère valeur ajoutée à la base.

D'autres fois, l'interface de classe de base est tellement stable que le couplage peut être considéré comme un problème. Cela est particulièrement vrai des classes telles que les conteneurs standard.

En résumé, la dérivation publique est un moyen rapide d'obtenir ou de se rapprocher de l'interface de classe de base idéale et familière pour la classe dérivée - de manière concise et bien évidemment correcte pour le mainteneur et le codeur client - avec des fonctionnalités supplémentaires disponibles en tant que fonctions membres ( lequel IMHO - qui diffère évidemment de Sutter, Alexandrescu, etc. - peut faciliter la convivialité, la lisibilité et aider les outils d'amélioration de la productivité, y compris les IDE)

Normes de codage C++ - Sutter & Alexandrescu - contre examinés

Le point 35 de Normes de codage C++ répertorie les problèmes liés au scénario de dérivation de std::string. En tant que scénarios, il est bon qu’il illustre la charge que représente la présentation d’une API volumineuse mais utile, mais qu’il soit bon ou non, l’API de base étant remarquablement stable, elle fait partie de la bibliothèque standard. Une base stable est une situation courante, mais pas plus commune qu'une base volatile et une bonne analyse doit porter sur les deux cas. Tout en examinant la liste de problèmes du livre, je vais spécifiquement contraster l'applicabilité de ces problèmes aux cas suivants:

a) class Issue_Id : public std::string { ...handy stuff... }; <- dérivation publique, notre utilisation controversée
b) class Issue_Id : public string_with_virtual_destructor { ...handy stuff... }; <- plus sûr OO dérivation
c) class Issue_Id { public: ...handy stuff... private: std::string id_; }; <- une approche compositionnelle
d) utiliser std::string partout, avec des fonctions de support autonomes

(Nous espérons pouvoir convenir que la composition est une pratique acceptable, car elle fournit une encapsulation, une sécurité de type et une API potentiellement enrichie, allant au-delà de celle de std::string.)

Alors, disons que vous écrivez un nouveau code et commencez à penser aux entités conceptuelles dans un sens OO. Peut-être que dans un système de suivi de bogues (je pense à JIRA), l’un d’eux est, par exemple, un Issue_Id. Le contenu des données est textuel - il se compose d'un identifiant de projet alphabétique, d'un trait d'union et d'un numéro d'édition incrémenté: par ex. "MYAPP-1234". Les identifiants de problème peuvent être stockés dans un std::string, et il y aura beaucoup de recherches de texte très simples et d'opérations de manipulation nécessaires sur les identifiants de problème - un grand sous-ensemble de ceux déjà fournis sur std::string et quelques autres pour faire bonne mesure (par exemple, obtenir le composant d'identifiant de projet , fournissant le prochain numéro d'identification possible (MYAPP-1235)).

Sur la liste de problèmes de Sutter et Alexandrescu ...

Les fonctions non membres fonctionnent bien dans le code existant qui manipule déjà strings. Si, à la place, vous fournissez un super_string, vous forcez les modifications dans votre base de code pour modifier les types et les signatures de fonction en super_string.

L'erreur fondamentale avec cette affirmation (et la plupart des affirmations ci-dessous) est qu'elle favorise la facilité d'utilisation de seulement quelques types, ignorant les avantages de la sécurité du type. Cela exprime une préférence pour d) ci-dessus, plutôt que de comprendre c) ou b) au lieu de a). L'art de la programmation consiste à équilibrer le pour et le contre de types distincts pour obtenir une réutilisation, une performance, une commodité et une sécurité raisonnables. Les paragraphes ci-dessous développent ceci.

En utilisant la dérivation publique, le code existant peut implicitement accéder à la classe de base string en tant que string et continuer à se comporter comme il l'a toujours fait. Il n'y a aucune raison particulière de penser que le code existant voudrait utiliser une fonctionnalité supplémentaire de super_string (dans notre cas Issue_Id) ... en fait, il s'agit souvent d'un code de support de niveau inférieur préexistant à l'application pour laquelle vous créez le super_string. , et donc inconscient des besoins prévus par les fonctions étendues. Par exemple, supposons qu'il existe une fonction non membre to_upper(std::string&, std::string::size_type from, std::string::size_type to) - elle pourrait toujours être appliquée à un Issue_Id.

Ainsi, à moins que la fonction de support des non-membres ne soit nettoyée ou étendue au coût délibéré d’un couplage étroit avec le nouveau code, elle n’a pas besoin d’être touchée. Si is est en cours de révision pour prendre en charge les identifiants de problème (par exemple, en utilisant la présentation du format de contenu des données en caractères majuscules uniquement), il est probablement judicieux de s’assurer qu’il est réellement en cours de traitement. passé un Issue_Id en créant une surcharge ala to_upper(Issue_Id&) et en restant fidèle aux approches de dérivation ou de composition permettant la sécurité du type. L'utilisation de super_string ou de la composition ne fait aucune différence en termes d'effort ou de maintenabilité. Une fonction de support autonome réutilisable to_upper_leading_alpha_only(std::string&) ne sera probablement pas très utile - je ne me souviens pas de la dernière fois où je voulais une telle fonction.

L'impulsion d'utiliser std::string partout n'est pas qualitativement différente de l'acceptation de tous vos arguments en tant que conteneurs de variantes ou void*s; vous n'avez donc pas à modifier vos interfaces pour accepter des données arbitraires, mais cela permet une implémentation sujette aux erreurs et une auto-documentation moins importante. code vérifiable par le compilateur.

Les fonctions d'interface qui prennent une chaîne doivent maintenant: a) rester à l'écart de la fonctionnalité ajoutée de super_string (inutile); b) copier leurs arguments dans une super chaîne (inutile); ou c) transformez la référence de chaîne en une référence super_string (maladroite et potentiellement illégale).

Cela semble revisiter le premier point - l'ancien code qui doit être refactorisé pour utiliser la nouvelle fonctionnalité, bien que cette fois le code client plutôt que le code de support. Si la fonction veut commencer à traiter son argument comme une entité pour laquelle les nouvelles opérations sont pertinentes, elle devrait commence à prendre ses arguments comme ce type et les clients doivent les générer. et les accepter en utilisant ce type. Les mêmes problèmes existent pour la composition. Sinon, c) peut être pratique et sûr si les directives énumérées ci-dessous sont suivies, même si elles sont laides.

les fonctions membres de super_string n'ont pas plus d'accès aux fonctions internes de string que les fonctions non membres, car string n'a probablement pas de membres protégés (rappelez-vous, elle n'était pas censée être dérivée à l'origine)

C'est vrai, mais parfois c'est une bonne chose. Beaucoup de classes de base n'ont pas de données protégées. L’interface publique string est tout ce qui est nécessaire pour manipuler le contenu, et des fonctionnalités utiles (par exemple, get_project_id() postulé ci-dessus) peuvent être élégamment exprimées en termes de ces opérations. Sur le plan conceptuel, bien souvent, j'ai dérivé des conteneurs Standard, mais je souhaitais ne pas étendre leurs fonctionnalités, ni les personnaliser sur les lignes existantes - ils sont déjà "parfaits" - mais plutôt ajouter une autre dimension de comportement spécifique. à mon application, et ne nécessite aucun accès privé. C'est parce qu'ils sont déjà bons qu'ils sont bons à réutiliser.

Si super_string masque certaines des fonctions de string (et redéfinir une fonction non virtuelle dans une classe dérivée n'est pas primordial, c'est simplement masquer), cela pourrait causer une confusion généralisée dans le code qui manipule strings qui ont commencé leur vie converti automatiquement de super_strings.

Cela vaut également pour la composition - et plus susceptible de se produire, car le code ne transmet pas les éléments par défaut, ce qui permet de rester synchronisé, et cela est également vrai dans certaines situations avec des hiérarchies polymorphes au moment de l'exécution. Samed a nommé les fonctions qui se comportent différemment dans les classes qui paraissent au départ interchangeables - tout simplement désagréables. C’est effectivement la mise en garde habituelle pour une programmation correcte OO, et encore une fois pas une raison suffisante pour abandonner les avantages en termes de sécurité de type, etc.

Que faire si super_string veut hériter de string en ajouter plus état [explication du découpage]

Convenu - ce n’est pas une bonne situation, et quelque part j’ai personnellement tendance à tracer la ligne, car cela déplace souvent les problèmes de suppression par le biais d’un pointeur allant du domaine théorique au très pratique - les destructeurs ne sont pas invoqués pour des membres supplémentaires. Néanmoins, le découpage en tranches peut souvent faire ce que l'on souhaite - étant donné que super_string est dérivé, il ne consiste pas à modifier les fonctionnalités héritées, mais à ajouter une autre "dimension" des fonctionnalités spécifiques à l'application ....

Certes, il est fastidieux de devoir écrire des fonctions de relais pour les fonctions membres que vous souhaitez conserver, mais une telle implémentation est nettement meilleure et plus sûre que d'utiliser un héritage public ou non public.

Eh bien, certainement d'accord sur l'ennui ...

Instructions pour une dérivation réussie sans destructeur virtuel

  • dans l’idéal, évitez d’ajouter des membres de données dans une classe dérivée: des variantes du découpage peuvent supprimer accidentellement des membres de données, les corrompre, ne pas les initialiser ...
  • encore plus - évitez les membres de données non-POD: la suppression via le pointeur de la classe de base est un comportement techniquement indéfini, mais avec des types non-POD qui échouent à exécuter leurs destructeurs, il est plus probable que des problèmes non théoriques avec des fuites de ressources se produisent. etc.
  • honorez le directeur de substitution de Liskov/vous ne pouvez pas maintenir de manière robuste de nouveaux invariants
    • par exemple, en dérivant de std::string, vous ne pouvez pas intercepter quelques fonctions et attendez que vos objets restent en majuscule: tout code qui y accède via un std::string& ou ...* peut utiliser les implémentations de fonctions originales de std::string pour modifier la valeur.
    • dériver pour modéliser une entité de niveau supérieur dans votre application, pour étendre la fonctionnalité héritée avec certaines fonctionnalités qui utilisent mais ne sont pas en conflit avec la base; n'attendez pas ou n'essayez pas de modifier les opérations de base - et l'accès à ces opérations - accordées par le type de base
  • soyez conscient du couplage: la classe de base ne peut pas être supprimée sans affecter le code client même si la fonctionnalité de la classe de base évolue de manière inappropriée, c'est-à-dire que la facilité d'utilisation de votre classe dérivée dépend de la pertinence continue de la base
    • parfois, même si vous utilisez une composition, vous devez exposer le membre de données en raison de performances, de problèmes de sécurité des threads ou d'un manque de sémantique de valeur. Ainsi, la perte d'encapsulation due à la dérivation publique n'est pas plus grave.
  • plus les personnes qui utilisent la classe potentiellement dérivée sont susceptibles d'ignorer ses compromis d'implémentation, moins vous pouvez vous permettre de les rendre dangereuses
    • par conséquent, les bibliothèques de bas niveau largement déployées avec de nombreux utilisateurs occasionnels ad-hoc devraient se méfier des dérivations dangereuses plutôt que d'une utilisation localisée par les programmeurs utilisant régulièrement les fonctionnalités au niveau de l'application et/ou dans des implémentations/bibliothèques "privées"

Résumé

Une telle dérivation n’est pas sans problèmes, ne la considérez donc pas à moins que le résultat final ne justifie les moyens. Cela dit, je rejette catégoriquement toute affirmation selon laquelle cela ne peut pas être utilisé en toute sécurité et de manière appropriée dans des cas particuliers - il s’agit simplement de tracer la ligne de démarcation.

Expérience personnelle

Je dérive parfois de std::map<>, std::vector<>, std::string etc. - Je n’ai jamais été brûlé par les problèmes de découpage ou de suppression par le biais de la classe de base, et j’ai économisé beaucoup de temps et d’énergie pour des tâches plus importantes. Je ne stocke pas de tels objets dans des conteneurs polymorphes hétérogènes. Cependant, vous devez vous demander si tous les programmeurs utilisant l'objet sont conscients des problèmes et sont susceptibles de programmer en conséquence. Personnellement, j'aime bien écrire mon code pour utiliser le polymorphisme des tas et de l'exécution uniquement lorsque cela est nécessaire, alors que certaines personnes (en raison de l'arrière-plan Java, de leur approche préférée pour gérer les dépendances de recompilation ou basculer entre les comportements d'exécution, les installations de test, etc. .) les utilisent habituellement et doivent donc se préoccuper davantage de la sécurité des opérations via les pointeurs de classe de base.

24
Tony Delroy

Non seulement le destructeur n'est pas virtuel, mais std :: string ne contient aucune fonction virtuelle du tout , et aucun membre protégé. Cela rend très difficile pour la classe dérivée de modifier ses fonctionnalités.

Alors pourquoi en tireriez-vous?

Un autre problème lié à la non-polymorphie est que si vous transmettez votre classe dérivée à une fonction qui attend un paramètre de chaîne, vos fonctionnalités supplémentaires seront simplement coupées en tranches et l'objet sera à nouveau vu comme une chaîne.

10
Bo Persson

Si vous vraiment voulez en déduire (sans expliquer pourquoi vous voulez le faire), je pense que vous pouvez empêcher l'instanciation directe de tas de la classe Derived en rendant operator new privé:

class StringDerived : public std::string {
//...
private:
  static void* operator new(size_t size);
  static void operator delete(void *ptr);
}; 

Mais de cette façon, vous vous limitez à tout objet StringDerived dynamique.

9
beduin

Pourquoi ne devrait-on pas dériver de la classe de chaînes c ++ std?

Parce que c'est pas nécessaire . Si vous souhaitez utiliser DerivedString pour l'extension de fonctionnalité; Je ne vois aucun problème à dériver std::string. La seule chose à faire est que vous ne devriez pas interagir entre les deux classes (c.-à-d. N'utilisez pas string comme récepteur pour DerivedString).

Est-il possible d'empêcher le client de faire Base* p = new Derived()

Oui . Assurez-vous de fournir les wrappers inline autour des méthodes Base dans la classe Derived. par exemple.

class Derived : protected Base { // 'protected' to avoid Base* p = new Derived
  const char* c_str () const { return Base::c_str(); }
//...
};
4
iammilind

Il y a deux raisons simples pour ne pas dériver d'une classe non polymorphe:

  • Technical: il introduit des bogues de découpage (car en C++ on passe par valeur sauf indication contraire)
  • Functional: s'il est non polymorphe, vous pouvez obtenir le même effet avec la composition et certains transferts de fonctions.

Si vous souhaitez ajouter de nouvelles fonctionnalités à std::string, envisagez d’abord d’utiliser des fonctions libres (éventuellement des modèles), comme le fait la bibliothèque Boost String Algorithm .

Si vous souhaitez ajouter de nouveaux membres aux données, encapsulez correctement l'accès aux classes en l'intégrant (Composition) dans une classe de votre propre conception.

MODIFIER:

@Tony a remarqué à juste titre que la fonctionnelle raison que j'ai citée n'avait probablement aucun sens pour la plupart des gens. Il existe une règle simple, bien conçue, qui dit que lorsque vous pouvez choisir une solution parmi plusieurs solutions, vous devez envisager celle avec le couplage le plus faible. La composition a un couplage plus faible que l'héritage, et devrait donc être préférée, lorsque cela est possible.

En outre, la composition vous offre la possibilité d’envelopper joliment la méthode de classe de l’original. Cela n'est pas possible si vous choisissez héritage (public) et que les méthodes ne sont pas virtuelles (ce qui est le cas ici).

2
Matthieu M.

Dès que vous ajoutez un membre (variable) dans votre classe dérivée std :: string, visez-vous systématiquement la pile si vous essayez d'utiliser les goodies std avec une instance de votre classe dérivée std :: string? Parce que les fonctions/membres de stdc ++ ont leurs pointeurs de pile [indexes] fixés [et ajustés] à la taille/limite de la taille de l'instance (base std :: string). 

Droite?

S'il vous plait corrigez moi si je me trompe.

0
Bretzelus

La norme C++ stipule que si le destructeur de la classe de base n'est pas virtuel et que vous supprimez un objet de la classe de base qui pointe vers l'objet d'une classe dérivée, un comportement non défini est créé.

Section 5.3.5/3 de la norme C++:

si le type statique de l'opérande est différent de son type dynamique, le type statique doit être une classe de base du type dynamique de l'opérande et le type statique doit avoir un destructeur virtuel ou le comportement n'est pas défini.

Pour être clair sur la classe non polymorphe et le besoin de destructeur virtuel
Le but de rendre un destructeur virtuel est de faciliter la suppression polymorphe d'objets par le biais de delete-expression. S'il n'y a pas de suppression polymorphe d'objets, alors vous n'avez pas besoin de destructor virtuel.

Pourquoi ne pas dériver de String Class?
On devrait généralement éviter de dériver de toute classe de conteneur standard à cause de la raison même pour laquelle ils n’ont pas de destructeurs virtuels, ce qui rend impossible la suppression polymorphe des objets.
En ce qui concerne la classe string, la classe string n'a aucune fonction virtuelle, il n'y a donc rien que vous puissiez éventuellement écraser. Le mieux que vous puissiez faire est de cacher quelque chose.

Si vous voulez avoir une chaîne comme une fonctionnalité, vous devriez écrire votre propre classe plutôt que d’hériter de std :: string.

0
Alok Save