Classes de base abstraites et construction de copie, règles générales

10

Souvent, c'est une bonne idée d'avoir une classe de base abstraite pour isoler l'interface de l'objet.

Le problème est que la construction de copie, à mon humble avis, est à peu près cassée par défaut en C ++, avec des constructeurs de copie générés par défaut.

Alors, quels sont les pièges lorsque vous avez une classe de base abstraite et des pointeurs bruts dans les classes dérivées?

class IAbstract
{
    ~IAbstract() = 0;
}

class Derived : public IAbstract
{
    char *theProblem;
    ...
}

IAbstract *a1 = new Derived();
IAbstract a2 = *a1;//???

Et maintenant, désactivez-vous proprement la construction de copie pour toute la hiérarchie? Déclarez que la construction de la copie est privée dans IAbstract?

Existe-t-il des règles de trois avec des classes de base abstraites?

Codeur
la source
1
utiliser des références au lieu de pointeurs :)
tp1
@ tp1: ou un pointeur intelligent au moins.
Benjamin Bannier
1
Parfois, il suffit de travailler avec du code existant ... Vous ne pouvez pas tout changer en un instant.
Coder
Pourquoi pensez-vous que le constructeur de copie par défaut est cassé?
BЈовић
2
@Coder: Le guide de style Google est un tas de déchets et est absolument nul pour tout développement C ++.
DeadMG

Réponses:

6

La construction de copie sur une classe abstraite doit être rendue privée dans la plupart des cas, ainsi que l'opérateur d'affectation.

Les classes abstraites sont, par définition, faites pour être un type polymorphe. Vous ne savez donc pas combien de mémoire votre instance utilise et vous ne pouvez donc pas le copier ou l'affecter en toute sécurité. En pratique, vous risquez de trancher: /programming/274626/what-is-the-slicing-problem-in-c

Le type polymorphe, en C ++, ne doit pas être manipulé par valeur. Vous les manipulez par référence ou par pointeur (ou tout autre pointeur intelligent).

C'est la raison pour laquelle Java a rendu l'objet manipulable uniquement par référence, et pourquoi C # et D ont la séparation entre les classes et les structures (le premier étant polymorphe et de type référence, le second étant non polymorphe et de type valeur).

deadalnix
la source
2
Les choses en Java ne sont pas meilleures. En Java, il est difficile de copier quoi que ce soit, même lorsque vous en avez vraiment, vraiment besoin, et très facile d'oublier de le faire. Vous vous retrouvez donc avec des bugs désagréables où deux structures de données ont une référence à la même classe de valeur (par exemple Date), puis l'une d'elles modifie la date et l'autre structure de données est maintenant cassée.
Kevin Cline du
3
Meh. Ce n'est pas un problème - quiconque connaît le C ++ ne fera pas cette erreur. Plus important encore, il n'y a aucune raison de manipuler les types polymorphes par valeur si vous savez ce que vous faites. Vous lancez une réaction totale de genou sur une possibilité techniquement mince. Les pointeurs NULL sont un problème plus important.
DeadMG
8

Vous pouvez, bien sûr, simplement le rendre protégé et vide, afin que les classes dérivées puissent choisir. Cependant, plus généralement, votre code est de toute façon interdit car il est impossible à instancier IAbstract- car il a une fonction virtuelle pure. En tant que tel, cela n'est généralement pas un problème - vos classes d'interface ne peuvent pas être instanciées et ne peuvent donc jamais être copiées, et vos classes plus dérivées peuvent interdire ou continuer à copier comme elles le souhaitent.

DeadMG
la source
Et que se passe-t-il s'il existe une hiérarchie Résumé -> Derived1 -> Derived2 et que Derived2 est affecté à Derived1? Ces coins sombres de la langue me surprennent encore.
Coder
@Coder: C'est généralement considéré comme PEBKAC. Cependant, plus directement, vous pouvez toujours déclarer un privé operator=(const Derived2&)dans Derived1.
DeadMG
Ok, ce n'est peut-être pas vraiment un problème. Je veux juste m'assurer que la classe est à l'abri des abus.
Coder
2
Et pour cela, vous devriez obtenir une médaille.
World Engineer
2
@Coder: abus par qui? Le mieux que vous puissiez faire est de faciliter l'écriture du code correct. Il est inutile d'essayer de se défendre contre les programmeurs qui ne connaissent pas le C ++.
kevin cline
2

En rendant ctor et affectation privés (ou en les déclarant comme = supprimer en C ++ 11), vous désactivez la copie.

Le point ici est OERE vous devez faire cela. Pour rester avec votre code, IAbstract n'est pas un problème. (notez qu'en faisant ce que vous avez fait, vous affectez le *a1 IAbstractsous - objet à a2, en perdant toute référence à Derived. L'affectation de valeur n'est pas polymorphe)

Le problème vient avec Derived::theproblem. Copier un dérivé dans un autre peut en fait partager les *theproblemdonnées qui ne sont pas conçues pour être partagées (il existe deux instances qui peuvent appeler delete theproblemleur destructeur).

Si tel est le cas, c'est Derivedqu'il doit être non copiable et non assignable. Bien sûr, si vous rendez la copie privée IAbstract, puisque la copie par défaut en a Derivedbesoin, elle Derivedne sera pas non plus copiable. Mais si vous définissez le vôtre Derived::Derived(const Derived&)sans appeler IAbtractcopy, vous pouvez toujours les copier.

Le problème est dans Derived, et la solution doit rester dans Derived: s'il doit s'agir d'un objet dynamique uniquement accessible uniquement par des pointeurs ou des références, c'est Derived lui-même qui doit avoir

class Derived
{
    ...
    Derived(const Derived&) = delete;
    Derived& operator=(const Derived&) = delete;
};

Il appartient essentiellement au concepteur de la classe Derived (qui devrait savoir comment fonctionne Derived et comment theproblemest géré) de décider quoi faire de l'affectation et de la copie.

Emilio Garavaglia
la source