Jemand erwähnte es im IRC als das Schnittproblem.
Jemand erwähnte es im IRC als das Schnittproblem.
Antworten:
Beim "Schneiden" weisen Sie einer Instanz einer Basisklasse ein Objekt einer abgeleiteten Klasse zu, wodurch ein Teil der Informationen verloren geht - ein Teil davon wird "geschnitten".
Zum Beispiel,
class A {
int foo;
};
class B : public A {
int bar;
};
Ein Objekt vom Typ B
hat also zwei Datenelemente foo
und bar
.
Dann, wenn Sie dies schreiben würden:
B b;
A a = b;
Dann gehen die Informationen b
über das Mitglied bar
in verloren a
.
A a = b;
a
ist jetzt ein Objekt vom Typ, A
das eine Kopie von hat B::foo
. Es wird ein Fehler sein, es jetzt zurück zu werfen, denke ich.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Man könnte denken , Sie kopiert haben , b1
zu b2
, aber Sie haben nicht! Sie haben einen Teil von b1
to b2
(den Teil von dem b1
, der von B
geerbt wurde A
) kopiert und die anderen Teile von b2
unverändert gelassen . b2
ist jetzt eine frankensteinische Kreatur, die aus ein paar b1
Teilen besteht, gefolgt von einigen Stücken b2
. Pfui! Downvoting, weil ich denke, dass die Antwort sehr irreführend ist.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
" Das eigentliche Problem tritt auf, wenn Sie " ... von einer Klasse mit einem nicht virtuellen Zuweisungsoperator stammen. Ist A
sogar zur Ableitung gedacht? Es hat keine virtuellen Funktionen. Wenn Sie von einem Typ abgeleitet sind, müssen Sie sich damit auseinandersetzen, dass seine Mitgliedsfunktionen aufgerufen werden können!
Die meisten Antworten hier erklären nicht, was das eigentliche Problem beim Schneiden ist. Sie erklären nur die gutartigen Fälle des Schneidens, nicht die tückischen. Nehmen Sie an, wie bei den anderen Antworten, dass Sie es mit zwei Klassen zu tun haben A
und B
woher B
(öffentlich) stammt A
.
In dieser Situation kann C ++ Sie eine Instanz übergeben B
zu A
‚s Zuweisungsoperator (und auch den Kopierkonstruktors). Dies funktioniert, weil eine Instanz von B
in a konvertiert werden kann. Dies const A&
erwarten Zuweisungsoperatoren und Kopierkonstruktoren von ihren Argumenten.
B b;
A a = b;
Dort passiert nichts Schlimmes - Sie haben nach einer Instanz gefragt, von A
der es sich um eine Kopie handelt B
, und genau das erhalten Sie. Sicher, a
wird einige b
Mitglieder nicht enthalten , aber wie sollte es sein? Es ist einA
immerhin nicht B
, so hat es nicht einmal gehört , über diese Mitglieder, geschweige denn , wäre in der Lage , sie zu speichern.
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Sie könnten denken, dass b2
dies eine Kopie von b1
später sein wird. Aber leider nicht ! Wenn Sie es inspizieren, werden Sie feststellen, dass b2
es sich um eine frankensteinische Kreatur handelt, die aus einigen Stücken von b1
(den Stücken, B
die von ihnen erben) bestehtA
) und einigen Stücken b2
(der Stücke, die nur B
enthalten) besteht. Autsch!
Was ist passiert? Nun, C ++ behandelt Zuweisungsoperatoren standardmäßig nicht als virtual
. Somit a_ref = b1
ruft die Leitung den Zuweisungsoperator von auf A
, nicht den von B
. Dies liegt daran, dass für nicht virtuelle Funktionen der deklarierte (formal: statische ) Typ (der ist A&
) bestimmt, welche Funktion aufgerufen wird, im Gegensatz zum tatsächlichen (formal: dynamischen ) Typ (der wäre B
, da er a_ref
auf eine Instanz von verweist B
). . Jetzt A
weiß der Zuweisungsoperator offensichtlich nur über die in deklarierten Mitglieder Bescheid A
, sodass nur diese kopiert werden und die hinzugefügten Mitglieder B
unverändert bleiben.
Das Zuweisen nur zu Teilen eines Objekts ist normalerweise wenig sinnvoll, aber C ++ bietet leider keine integrierte Möglichkeit, dies zu verbieten. Sie können jedoch Ihre eigenen rollen. Der erste Schritt besteht darin, den Zuweisungsoperator virtuell zu machen . Dies garantiert, dass immer der Zuweisungsoperator des tatsächlichen Typs aufgerufen wird, nicht der deklarierte Typ. Der zweite Schritt besteht darin, dynamic_cast
zu überprüfen, ob das zugewiesene Objekt einen kompatiblen Typ hat. Der dritte Schritt besteht darin, die eigentliche Zuweisung in einem (geschützten!) Mitglied durchzuführen assign()
, da B
's assign()
wahrscheinlich A
' s assign()
zum Kopieren von A
Mitgliedern verwenden möchte .
class A {
public:
virtual A& operator= (const A& a) {
assign(a);
return *this;
}
protected:
void assign(const A& a) {
// copy members of A from a to this
}
};
class B : public A {
public:
virtual B& operator= (const A& a) {
if (const B* b = dynamic_cast<const B*>(&a))
assign(*b);
else
throw bad_assignment();
return *this;
}
protected:
void assign(const B& b) {
A::assign(b); // Let A's assign() copy members of A from b to this
// copy members of B from b to this
}
};
Beachten Sie, dass, für die reine Bequemlichkeit B
‚s operator=
kovariant überschreibt die Art Rückkehr, da er weiß , dass es eine Instanz ist zurückkehrt B
.
derived
kann dem Code, der einen base
Wert erwartet, ein beliebiger Wert zugewiesen werden , oder es kann eine abgeleitete Referenz als Basisreferenz verwendet werden. Ich würde gerne eine Sprache mit einem Typsystem sehen, das beide Konzepte getrennt behandelt. Es gibt viele Fälle, in denen eine abgeleitete Referenz durch eine Basisreferenz ersetzt werden sollte, abgeleitete Instanzen jedoch nicht durch Basisinstanzen ersetzt werden sollten. Es gibt auch viele Fälle, in denen Instanzen konvertierbar sein sollten, Referenzen jedoch nicht ersetzen sollten.
Wenn Sie eine Basisklasse A
und eine abgeleitete Klasse haben B
, können Sie Folgendes tun.
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
Jetzt benötigt die Methode wantAnA
eine Kopie von derived
. Das Objekt derived
kann jedoch nicht vollständig kopiert werden, da die Klasse B
zusätzliche Elementvariablen erfinden könnte, die sich nicht in ihrer Basisklasse befinden A
.
Zum Aufrufen wantAnA
"schneidet" der Compiler daher alle zusätzlichen Mitglieder der abgeleiteten Klasse ab. Das Ergebnis könnte ein Objekt sein, das Sie nicht erstellen wollten, weil
A
-Objekt (alles spezielle Verhalten der Klasse B
geht verloren).wantAnA
(wie der Name schon sagt!) Einen will A
, dann ist es das, was es bekommt. Und eine Instanz von A
wird sich wie eine verhalten A
. Wie ist das überraschend?
derived
für den Typ ausführt A
. Implizites Casting ist in C ++ immer eine Quelle unerwarteten Verhaltens, da es oft schwer zu verstehen ist, wenn man den Code lokal betrachtet, dass ein Casting stattgefunden hat.
Das sind alles gute Antworten. Ich möchte nur ein Ausführungsbeispiel hinzufügen, wenn Objekte nach Wert und Referenz übergeben werden:
#include <iostream>
using namespace std;
// Base class
class A {
public:
A() {}
A(const A& a) {
cout << "'A' copy constructor" << endl;
}
virtual void run() const { cout << "I am an 'A'" << endl; }
};
// Derived class
class B: public A {
public:
B():A() {}
B(const B& a):A(a) {
cout << "'B' copy constructor" << endl;
}
virtual void run() const { cout << "I am a 'B'" << endl; }
};
void g(const A & a) {
a.run();
}
void h(const A a) {
a.run();
}
int main() {
cout << "Call by reference" << endl;
g(B());
cout << endl << "Call by copy" << endl;
h(B());
}
Die Ausgabe ist:
Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A'
Das dritte Match in Google für "C ++ Slicing" gibt mir diesen Wikipedia-Artikel http://en.wikipedia.org/wiki/Object_slicing und diesen (hitzig, aber die ersten paar Beiträge definieren das Problem): http://bytes.com/ forum / thread163565.html
Es ist also, wenn Sie der Superklasse ein Objekt einer Unterklasse zuweisen. Die Oberklasse kennt die zusätzlichen Informationen in der Unterklasse nicht und hat keinen Platz zum Speichern. Daher werden die zusätzlichen Informationen "abgeschnitten".
Wenn diese Links nicht genügend Informationen für eine "gute Antwort" enthalten, bearbeiten Sie bitte Ihre Frage, um uns mitzuteilen, wonach Sie mehr suchen.
Das Slicing-Problem ist schwerwiegend, da es zu einer Speicherbeschädigung führen kann und es sehr schwierig ist, sicherzustellen, dass ein Programm nicht darunter leidet. Um es außerhalb der Sprache zu entwerfen, sollten Klassen, die die Vererbung unterstützen, nur als Referenz (nicht nach Wert) zugänglich sein. Die Programmiersprache D hat diese Eigenschaft.
Betrachten Sie Klasse A und Klasse B, die von A abgeleitet sind. Eine Speicherbeschädigung kann auftreten, wenn der A-Teil einen Zeiger p und eine B-Instanz hat, die p auf die zusätzlichen Daten von B zeigt. Wenn dann die zusätzlichen Daten abgeschnitten werden, zeigt p auf Müll.
Derived
implizit in konvertiert werden können Base
.) Dies widerspricht offensichtlich dem Open-Closed-Prinzip und ist mit einem hohen Wartungsaufwand verbunden.
In C ++ kann ein abgeleitetes Klassenobjekt einem Basisklassenobjekt zugewiesen werden, der andere Weg ist jedoch nicht möglich.
class Base { int x, y; };
class Derived : public Base { int z, w; };
int main()
{
Derived d;
Base b = d; // Object Slicing, z and w of d are sliced off
}
Das Aufteilen von Objekten erfolgt, wenn ein abgeleitetes Klassenobjekt einem Basisklassenobjekt zugewiesen wird. Zusätzliche Attribute eines abgeleiteten Klassenobjekts werden abgeschnitten, um das Basisklassenobjekt zu bilden.
Das Slicing-Problem in C ++ ergibt sich aus der Wertesemantik seiner Objekte, die hauptsächlich aufgrund der Kompatibilität mit C-Strukturen erhalten blieb. Sie müssen eine explizite Referenz- oder Zeigersyntax verwenden, um ein "normales" Objektverhalten zu erzielen, das in den meisten anderen Sprachen für Objekte zu finden ist, dh Objekte werden immer als Referenz weitergegeben.
Die kurze Antwort lautet, dass Sie das Objekt in Scheiben schneiden, indem Sie einem Basisobjekt ein abgeleitetes Objekt nach Wert zuweisen , dh das verbleibende Objekt ist nur ein Teil des abgeleiteten Objekts. Um die Wertesemantik zu erhalten, ist das Schneiden ein vernünftiges Verhalten und hat relativ seltene Verwendungszwecke, die in den meisten anderen Sprachen nicht vorhanden sind. Einige Leute betrachten es als eine Funktion von C ++, während viele es als eine der Macken / Fehlfunktionen von C ++ betrachteten.
struct
, Kompatibilität oder andere Unsinnigkeiten, die Ihnen ein zufälliger OOP-Priester gesagt hat.
Base
genau sizeof(Base)
Bytes im Speicher benötigen muss , mit möglicher Ausrichtung, möglicherweise deshalb "Zuweisung" (Kopieren auf dem Stapel) ) kopiert keine abgeleiteten Klassenmitglieder, ihre Offsets liegen außerhalb der Größe von. Um "Datenverlust" zu vermeiden, verwenden Sie einfach wie alle anderen Zeiger, da der Zeigerspeicher an Ort und Stelle und in der Größe festgelegt ist, während der Stapel sehr flüchtig ist
Also ... Warum ist es schlecht, die abgeleiteten Informationen zu verlieren? ... weil der Autor der abgeleiteten Klasse die Darstellung möglicherweise so geändert hat, dass das Abschneiden der zusätzlichen Informationen den vom Objekt dargestellten Wert ändert. Dies kann passieren, wenn die abgeleitete Klasse zum Zwischenspeichern einer Darstellung verwendet wird, die für bestimmte Operationen effizienter ist, deren Rücktransformation in die Basisdarstellung jedoch teuer ist.
Ich dachte auch, jemand sollte auch erwähnen, was Sie tun sollten, um ein Schneiden zu vermeiden ... Holen Sie sich eine Kopie der C ++ - Codierungsstandards, 101 Richtlinien und Best Practices. Der Umgang mit dem Schneiden ist # 54.
Es wird ein etwas ausgefeiltes Muster vorgeschlagen, um das Problem vollständig zu lösen: einen geschützten Kopierkonstruktor, einen geschützten reinen virtuellen DoClone und einen öffentlichen Klon mit einer Zusicherung, die Ihnen sagt, ob eine (weitere) abgeleitete Klasse DoClone nicht korrekt implementiert hat. (Die Klonmethode erstellt eine korrekte tiefe Kopie des polymorphen Objekts.)
Sie können den Kopierkonstruktor auch explizit auf der Basis markieren, um bei Bedarf ein explizites Schneiden zu ermöglichen.
1. DIE DEFINITION DES SCHNITTPROBLEMS
Wenn D eine abgeleitete Klasse der Basisklasse B ist, können Sie einer Variablen (oder einem Parameter) vom Typ Base ein Objekt vom Typ Abgeleitet zuweisen.
BEISPIEL
class Pet
{
public:
string name;
};
class Dog : public Pet
{
public:
string breed;
};
int main()
{
Dog dog;
Pet pet;
dog.name = "Tommy";
dog.breed = "Kangal Dog";
pet = dog;
cout << pet.breed; //ERROR
Obwohl die obige Zuordnung zulässig ist, verliert der Wert, der dem variablen Haustier zugewiesen wird, sein Rassenfeld. Dies wird als Schneidproblem bezeichnet .
2. BEHEBEN DES SCHNITTPROBLEMS
Um das Problem zu beheben, verwenden wir Zeiger auf dynamische Variablen.
BEISPIEL
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;
In diesem Fall geht keines der Datenelemente oder Elementfunktionen der dynamischen Variablen verloren, auf die ptrD (untergeordnetes Klassenobjekt) zeigt. Wenn Sie Funktionen verwenden müssen, muss die Funktion außerdem eine virtuelle Funktion sein.
dog
nicht Teil der Klasse ist Pet
(das breed
Datenelement), nicht in die Variable kopiert wird pet
? Der Code interessiert sich Pet
anscheinend nur für die Datenelemente. Schneiden ist definitiv ein "Problem", wenn es unerwünscht ist, aber das sehe ich hier nicht.
((Dog *)ptrP)
Ich schlage vor, zu verwendenstatic_cast<Dog*>(ptrP)
Dog::breed
), in keiner Weise ein FEHLER im Zusammenhang mit SLICING ist?
Es scheint mir, dass das Schneiden nicht so sehr ein Problem ist, als wenn Ihre eigenen Klassen und Programme schlecht aufgebaut / entworfen sind.
Wenn ich ein Unterklassenobjekt als Parameter an eine Methode übergebe, die einen Parameter vom Typ Superklasse verwendet, sollte ich mir dessen bewusst sein und wissen, dass die aufgerufene Methode intern nur mit dem Objekt der Oberklasse (auch bekannt als Basisklasse) funktioniert.
Es scheint mir nur die unvernünftige Erwartung, dass die Bereitstellung einer Unterklasse, in der eine Basisklasse angefordert wird, irgendwie zu unterklassenspezifischen Ergebnissen führen und das Schneiden zu einem Problem machen würde. Es ist entweder ein schlechtes Design bei der Verwendung der Methode oder eine schlechte Implementierung der Unterklasse. Ich vermute, dass dies normalerweise das Ergebnis eines Opfers eines guten OOP-Designs zugunsten von Zweckmäßigkeit oder Leistungssteigerungen ist.
OK, ich werde es versuchen, nachdem ich viele Beiträge gelesen habe, in denen das Schneiden von Objekten erklärt wird, aber nicht, wie es problematisch wird.
Das bösartige Szenario, das zu einer Speicherbeschädigung führen kann, ist das folgende:
Slicing bedeutet, dass die von einer Unterklasse hinzugefügten Daten verworfen werden, wenn ein Objekt der Unterklasse übergeben oder als Wert oder von einer Funktion zurückgegeben wird, die ein Basisklassenobjekt erwartet.
Erläuterung: Beachten Sie die folgende Klassendeklaration:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Da die Kopierfunktionen der Basisklasse nichts über das Abgeleitete wissen, wird nur der Basisteil des Abgeleiteten kopiert. Dies wird üblicherweise als Schneiden bezeichnet.
class A
{
int x;
};
class B
{
B( ) : x(1), c('a') { }
int x;
char c;
};
int main( )
{
A a;
B b;
a = b; // b.c == 'a' is "sliced" off
return 0;
}
Wenn ein abgeleitetes Klassenobjekt einem Basisklassenobjekt zugewiesen wird, werden zusätzliche Attribute eines abgeleiteten Klassenobjekts vom Basisklassenobjekt abgeschnitten (verworfen).
class Base {
int x;
};
class Derived : public Base {
int z;
};
int main()
{
Derived d;
Base b = d; // Object Slicing, z of d is sliced off
}
Wenn dem Basisklassenobjekt ein abgeleitetes Klassenobjekt zugewiesen wird, werden alle Mitglieder des abgeleiteten Klassenobjekts in das Basisklassenobjekt kopiert, mit Ausnahme der Elemente, die in der Basisklasse nicht vorhanden sind. Diese Mitglieder werden vom Compiler entfernt. Dies wird als Objektschneiden bezeichnet.
Hier ist ein Beispiel:
#include<bits/stdc++.h>
using namespace std;
class Base
{
public:
int a;
int b;
int c;
Base()
{
a=10;
b=20;
c=30;
}
};
class Derived : public Base
{
public:
int d;
int e;
Derived()
{
d=40;
e=50;
}
};
int main()
{
Derived d;
cout<<d.a<<"\n";
cout<<d.b<<"\n";
cout<<d.c<<"\n";
cout<<d.d<<"\n";
cout<<d.e<<"\n";
Base b = d;
cout<<b.a<<"\n";
cout<<b.b<<"\n";
cout<<b.c<<"\n";
cout<<b.d<<"\n";
cout<<b.e<<"\n";
return 0;
}
Es wird generiert:
[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'
Ich bin gerade auf das Problem des Schneidens gestoßen und bin sofort hier gelandet. Lassen Sie mich also meine zwei Cent dazu addieren.
Lassen Sie uns ein Beispiel aus "Produktionscode" (oder etwas, das irgendwie nahe kommt) haben:
Nehmen wir an, wir haben etwas, das Aktionen auslöst. Eine Control Center-Benutzeroberfläche zum Beispiel.
Diese Benutzeroberfläche muss eine Liste der Dinge erhalten, die derzeit versendet werden können. Also definieren wir eine Klasse, die die Versandinformationen enthält. Nennen wir es Action
. Also Action
hat ein einige Mitgliedsvariablen. Der Einfachheit halber haben wir nur 2, a std::string name
und a std::function<void()> f
. Dann hat es eine, void activate()
die gerade die ausführtf
Mitglied .
So wird die Benutzeroberfläche std::vector<Action>
mitgeliefert. Stellen Sie sich einige Funktionen vor wie:
void push_back(Action toAdd);
Jetzt haben wir festgelegt, wie es aus der Sicht der Benutzeroberfläche aussieht. Bisher kein Problem. Aber ein anderer Typ, der an diesem Projekt arbeitet, entscheidet plötzlich, dass es spezielle Aktionen gibt, die mehr Informationen benötigenAction
Objekt . Aus welchem Grund auch immer. Das könnte auch mit Lambda-Captures gelöst werden. Dieses Beispiel wird nicht 1-1 aus dem Code übernommen.
Also leitet der Typ Action
seinen eigenen Geschmack ab.
Er gibt eine Instanz seiner selbst gebrauten Klasse an die weiter, push_back
aber dann geht das Programm durcheinander.
Also was ist passiert?
Wie Sie vielleicht erraten haben: Das Objekt wurde in Scheiben geschnitten.
Die zusätzlichen Informationen aus der Instanz sind verloren gegangen und f
neigen jetzt zu undefiniertem Verhalten.
Ich hoffe, dieses Beispiel bringt Licht für diejenigen Menschen, die sich Dinge nicht wirklich vorstellen können, wenn sie davon sprechen, dass A
s und B
s auf irgendeine Weise abgeleitet werden.