Abstrakte Basisklassen und Kopierkonstruktion, Faustregeln

9

Oft ist es eine gute Idee, eine abstrakte Basisklasse zu haben, um die Schnittstelle des Objekts zu isolieren.

Das Problem ist, dass die Kopierkonstruktion IMHO in C ++ standardmäßig ziemlich kaputt ist und standardmäßig Kopierkonstruktoren generiert werden.

Was sind die Fallstricke, wenn Sie eine abstrakte Basisklasse und Rohzeiger in abgeleiteten Klassen haben?

class IAbstract
{
    ~IAbstract() = 0;
}

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

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

Und jetzt deaktivieren Sie die Kopierkonstruktion für die gesamte Hierarchie sauber? Kopierkonstruktion als privat deklarieren in IAbstract?

Gibt es Dreierregeln mit abstrakten Basisklassen?

Codierer
quelle
1
Verwenden Sie Referenzen anstelle von Zeigern :)
TP1
@ tp1: oder zumindest ein intelligenter Zeiger.
Benjamin Bannier
1
Manchmal muss man nur mit vorhandenem Code arbeiten ... Man kann nicht alles sofort ändern.
Coder
Warum ist Ihrer Meinung nach der Standardkopierkonstruktor defekt?
BЈовић
2
@Coder: Der Google Style Guide ist ein Haufen Müll und für jede C ++ - Entwicklung absolut zum Kotzen.
DeadMG

Antworten:

6

Die Kopierkonstruktion für eine abstrakte Klasse sollte in den meisten Fällen privat gemacht werden, ebenso wie der Zuweisungsoperator.

Abstrakte Klassen sind per Definition ein polymorpher Typ. Sie wissen also nicht, wie viel Speicher Ihre Instanz verwendet, und können ihn daher nicht sicher kopieren oder zuweisen. In der Praxis riskieren Sie das Schneiden: /programming/274626/what-is-the-slicing-problem-in-c

Der polymorphe Typ in C ++ darf nicht durch den Wert manipuliert werden. Sie bearbeiten sie durch Referenz oder durch Zeiger (oder einen beliebigen intelligenten Zeiger).

Dies ist der Grund, warum Java Objekte nur durch Referenz manipulierbar gemacht hat und warum C # und D die Trennung zwischen Klassen und Strukturen aufweisen (die erste ist polymorph und der Referenztyp, die zweite ist nicht polymorph und der Werttyp).

deadalnix
quelle
2
Die Dinge in Java sind nicht besser. In Java ist es schmerzhaft, etwas zu kopieren, selbst wenn Sie es wirklich brauchen, und es ist sehr leicht zu vergessen, es zu tun. Es kommt also zu bösen Fehlern, bei denen zwei Datenstrukturen auf dieselbe Werteklasse (z. B. Datum) verweisen und eine von ihnen das Datum ändert und die andere Datenstruktur jetzt fehlerhaft ist.
Kevin Cline
3
Meh. Es ist kein Problem - jeder, der C ++ kennt, wird diesen Fehler nicht machen. Noch wichtiger ist, dass es überhaupt keinen Grund gibt, polymorphe Typen nach Wert zu manipulieren, wenn Sie wissen, was Sie tun. Sie lösen eine totale Knie-Ruck-Reaktion wegen einer technisch geringen Möglichkeit aus. NULL-Zeiger sind ein größeres Problem.
DeadMG
8

Sie können es natürlich auch nur geschützt und leer machen, damit abgeleitete Klassen auswählen können. Im Allgemeinen ist Ihr Code jedoch sowieso verboten, da er nicht instanziiert werden kann IAbstract- weil er eine rein virtuelle Funktion hat. Daher ist dies im Allgemeinen kein Problem. Ihre Schnittstellenklassen können nicht instanziiert und daher niemals kopiert werden, und Ihre abgeleiteten Klassen können das Kopieren nach Belieben verbieten oder fortsetzen.

DeadMG
quelle
Und was ist, wenn es eine Hierarchie von Abstract -> Derived1 -> Derived2 gibt und Derived2 Derived1 zugewiesen ist? Diese dunklen Ecken der Sprache überraschen mich immer noch.
Coder
@Coder: Das wird normalerweise als PEBKAC angesehen. Direkter können Sie jedoch jederzeit eine private operator=(const Derived2&)in deklarieren Derived1.
DeadMG
Ok, vielleicht ist das wirklich kein Problem. Ich möchte nur sicherstellen, dass die Klasse vor Missbrauch geschützt ist.
Coder
2
Und dafür solltest du eine Medaille bekommen.
Weltingenieur
2
@Coder: Missbrauch durch wen? Das Beste, was Sie tun können, ist, das Schreiben von korrektem Code zu vereinfachen. Es ist sinnlos, sich gegen Programmierer zu verteidigen, die C ++ nicht kennen.
Kevin Cline
2

Indem Sie ctor und die Zuweisung privat machen (oder sie in C ++ 11 als = delete deklarieren), deaktivieren Sie die Kopie.

Der Punkt hier ist, wo Sie das tun müssen. Um bei Ihrem Code zu bleiben, ist IAbstract kein Problem. (Beachten Sie, dass Sie bei dem, was Sie getan haben, das *a1 IAbstractUnterobjekt a2 zuweisen und dabei keinen Verweis auf verlieren Derived. Die Wertzuweisung ist nicht polymorph.)

Das Problem kommt mit Derived::theproblem. Das Kopieren eines Abgeleiteten in ein anderes kann tatsächlich die *theproblemDaten gemeinsam nutzen, die möglicherweise nicht für die gemeinsame Nutzung ausgelegt sind (es gibt zwei Instanzen, die delete theproblemihren Destruktor aufrufen können).

Wenn dies der Fall ist Derived, muss dies nicht kopierbar und nicht zuweisbar sein. Wenn Sie die Kopie privat machen, kann sie natürlich auch nicht kopiert werden IAbstract, da die Standardkopie sie Derivedbenötigt Derived. Wenn Sie jedoch Ihre eigenen definieren, Derived::Derived(const Derived&)ohne IAbtractcopy aufzurufen , können Sie sie dennoch kopieren.

Das Problem befindet sich in Abgeleitet, und die Lösung muss in Abgeleitet bleiben: Wenn es sich um ein Nur-Dynamik-Objekt handeln muss, auf das nur über Zeiger oder Referenzen zugegriffen werden kann, muss es Abgeleitet sein

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

Im Wesentlichen ist es theproblemSache des Designers der Klasse Derived (der wissen sollte, wie Derived funktioniert und wie verwaltet wird), zu entscheiden, was mit Zuweisung und Kopieren geschehen soll.

Emilio Garavaglia
quelle