Qualcuno lo ha menzionato nell'IRC come problema di taglio.
Qualcuno lo ha menzionato nell'IRC come problema di taglio.
Risposte:
"Slicing" è il punto in cui si assegna un oggetto di una classe derivata a un'istanza di una classe base, perdendo così parte delle informazioni, alcune delle quali vengono "tagliate" via.
Per esempio,
class A {
int foo;
};
class B : public A {
int bar;
};
Quindi un oggetto di tipo B
ha due membri di dati foo
e bar
.
Quindi se dovessi scrivere questo:
B b;
A a = b;
Quindi le informazioni in b
su membro bar
sono perse a
.
A a = b;
a
ora è un oggetto di tipo A
che ne ha una copia B::foo
. Sarà un errore riaverlo indietro ora penso.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Si potrebbe pensare che hai copiato b1
a b2
, ma non avere! È stato copiato un parte di b1
al b2
(la parte di b1
quello B
ereditato da A
), e ha lasciato le altre parti del b2
invariate. b2
è ora una creatura frankensteiniana composta da alcuni frammenti di b1
seguiti da alcuni pezzi di b2
. Ugh! Downvoting perché penso che la risposta sia molto fuorviante.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
" Il vero problema si verifica se tu " ... deriva da una classe con un operatore di assegnazione non virtuale. È A
anche destinato alla derivazione? Non ha funzioni virtuali. Se derivate da un tipo, dovete affrontare il fatto che le sue funzioni membro possono essere chiamate!
La maggior parte delle risposte non riesce a spiegare quale sia il vero problema con lo slicing. Spiegano solo i casi benigni di affettare, non quelli insidiosi. Supponi, come le altre risposte, che hai a che fare con due classi A
e B
, da dove B
deriva (pubblicamente) A
.
In questa situazione, C ++ permette di passare un'istanza di B
a A
's operatore di assegnazione (e anche per il costruttore di copia). Questo funziona perché un'istanza di B
può essere convertita in a const A&
, che è ciò che gli operatori di assegnazione e i costruttori di copie si aspettano che siano i loro argomenti.
B b;
A a = b;
Non succede nulla di brutto lì: hai chiesto un'istanza di A
cui è una copia B
, ed è esattamente quello che ottieni. Certo, a
non conterrà alcuni b
membri, ma come dovrebbe? Dopotutto è un A
non B
, quindi non ha nemmeno sentito parlare di questi membri, figuriamoci sarebbe in grado di memorizzarli.
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Potresti pensare che b2
sarà una copia di b1
dopo. Ma, ahimè, non lo è ! Se lo ispezionerai, scoprirai che b2
è una creatura frankensteiniana, composta da alcuni pezzi di b1
(i pezzi da cui B
eredita A
) e alcuni pezzi di b2
(i pezzi che B
contengono solo ). Ahia!
Quello che è successo? Bene, C ++ di default non tratta gli operatori di assegnazione come virtual
. Pertanto, la linea a_ref = b1
chiamerà l'operatore di assegnazione di A
, non quello di B
. Questo perché, per le funzioni non virtuali, il tipo dichiarato (formalmente: statico ) (che è A&
) determina quale funzione viene chiamata, al contrario del tipo effettivo (formalmente: dinamico ) (che sarebbe B
, dal momento che fa a_ref
riferimento a un'istanza di B
) . Ora, A
l'operatore di assegnazione ovviamente conosce solo i membri dichiarati in A
, quindi copia solo quelli, lasciando i membri aggiunti B
invariati.
L'assegnazione solo a parti di un oggetto di solito ha poco senso, ma C ++, sfortunatamente, non fornisce alcun modo incorporato per vietarlo. Tuttavia, puoi farlo tu stesso. Il primo passo è rendere virtuale l' operatore di assegnazione . Ciò garantirà che sia sempre chiamato l'operatore di assegnazione del tipo effettivo , non il tipo dichiarato . Il secondo passaggio consiste nell'utilizzare dynamic_cast
per verificare che l'oggetto assegnato abbia un tipo compatibile. Il terzo passo è quello di fare l'assegnazione effettiva a un membro (protetto!) assign()
, Dal momento che B
's assign()
probabilmente vorranno utilizzare A
' il assign()
copiare A
's, i membri.
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
}
};
Si noti che, per pura comodità, B
il operator=
comando covariant ha la precedenza sul tipo restituito, poiché sa che sta restituendo un'istanza di B
.
derived
può dare qualsiasi valore al codice in attesa di un base
valore, o qualsiasi riferimento derivato può essere usato come riferimento di base. Vorrei vedere una lingua con un sistema di tipi che affronta entrambi i concetti separatamente. Ci sono molti casi in cui un riferimento derivato dovrebbe essere sostituibile con un riferimento di base, ma le istanze derivate non dovrebbero essere sostituibili con un riferimento di base; ci sono anche molti casi in cui le istanze dovrebbero essere convertibili ma i riferimenti non dovrebbero sostituirsi.
Se hai una classe base A
e una classe derivata B
, puoi fare quanto segue.
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
Ora il metodo ha wantAnA
bisogno di una copia di derived
. Tuttavia, l'oggetto derived
non può essere copiato completamente, poiché la classe B
potrebbe inventare ulteriori variabili membro che non si trovano nella sua classe base A
.
Pertanto, per chiamare wantAnA
, il compilatore "troncerà" tutti i membri aggiuntivi della classe derivata. Il risultato potrebbe essere un oggetto che non volevi creare, perché
A
(tutto il comportamento speciale della classe B
viene perso).wantAnA
(come suggerisce il nome!) Vuole un A
, allora è quello che ottiene. E un'istanza di A
, si comporterà come una A
. Com'è sorprendente?
derived
tipo A
. Il cast implicito è sempre una fonte di comportamenti inaspettati in C ++, perché spesso è difficile capire guardando il codice localmente che ha avuto luogo un cast.
Queste sono tutte buone risposte. Vorrei solo aggiungere un esempio di esecuzione quando si passano oggetti per valore vs per riferimento:
#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());
}
L'output è:
Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A'
La terza corrispondenza su Google per "C ++ slicing" mi dà questo articolo di Wikipedia http://en.wikipedia.org/wiki/Object_slicing e questo (accesi, ma i primi post definiscono il problema): http://bytes.com/ forum / thread163565.html
Quindi è quando assegni un oggetto di una sottoclasse alla superclasse. La superclasse non è a conoscenza delle informazioni aggiuntive nella sottoclasse e non ha spazio per memorizzarle, quindi le informazioni aggiuntive vengono "troncate".
Se quei link non forniscono abbastanza informazioni per una "buona risposta", modifica la tua domanda per farci sapere che cosa stai cercando.
Il problema di slicing è grave perché può causare il danneggiamento della memoria ed è molto difficile garantire che un programma non ne risenta. Per progettarlo fuori dalla lingua, le classi che supportano l'ereditarietà dovrebbero essere accessibili solo come riferimento (non per valore). Il linguaggio di programmazione D ha questa proprietà.
Considerare la classe A e la classe B derivata da A. La corruzione della memoria può verificarsi se la parte A ha un puntatore p e un'istanza B che punta p ai dati aggiuntivi di B. Quindi, quando i dati aggiuntivi vengono troncati, p indica immondizia.
Derived
è implicitamente convertibile in Base
.) Ciò è ovviamente contrario al principio aperto-chiuso e un grande onere di manutenzione.
In C ++, un oggetto classe derivato può essere assegnato a un oggetto classe base, ma non è possibile viceversa.
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
}
La suddivisione in oggetti si verifica quando un oggetto di classe derivata viene assegnato a un oggetto di classe base, gli attributi aggiuntivi di un oggetto di classe derivata vengono troncati per formare l'oggetto di classe base.
Il problema di slicing in C ++ deriva dalla semantica di valore dei suoi oggetti, che è rimasta principalmente a causa della compatibilità con le strutture C. È necessario utilizzare il riferimento esplicito o la sintassi del puntatore per ottenere un comportamento "normale" degli oggetti che si trova nella maggior parte degli altri linguaggi che fanno oggetti, ovvero gli oggetti vengono sempre passati per riferimento.
La risposta breve è che si suddivide l'oggetto assegnando un oggetto derivato a un oggetto base per valore , ovvero l'oggetto rimanente è solo una parte dell'oggetto derivato. Al fine di preservare la semantica del valore, il slicing è un comportamento ragionevole e ha i suoi usi relativamente rari, che non esiste nella maggior parte delle altre lingue. Alcune persone lo considerano una caratteristica del C ++, mentre molti lo consideravano una delle stranezze / malfunzionamenti del C ++.
struct
, compatibilità o altri non-senso che qualsiasi sacerdote OOP casuale ti ha detto.
Base
deve richiedere esattamente sizeof(Base)
byte in memoria, con possibile allineamento, forse, ecco perché "assegnazione" (copia in pila ) non copierà i membri della classe derivati, i loro offset sono fuori dimensione. Per evitare "la perdita di dati", basta usare il puntatore, come chiunque altro, poiché la memoria del puntatore è fissata in posizione e dimensioni, mentre lo stack è molto volatile
Quindi ... Perché perdere le informazioni derivate è male? ... perché l'autore della classe derivata potrebbe aver modificato la rappresentazione in modo tale che tagliando le informazioni extra si modifica il valore rappresentato dall'oggetto. Ciò può accadere se la classe derivata viene utilizzata per memorizzare nella cache una rappresentazione più efficiente per determinate operazioni, ma costosa per tornare alla rappresentazione di base.
Ho anche pensato che qualcuno dovrebbe anche menzionare cosa dovresti fare per evitare lo slicing ... Ottieni una copia degli standard di codifica C ++, 101 linee guida delle regole e migliori pratiche. Trattare con affettare è # 54.
Suggerisce un modello un po 'sofisticato per affrontare completamente il problema: avere un costruttore di copie protetto, un DoClone virtuale puro protetto e un clone pubblico con un'asserzione che ti dirà se una (ulteriore) classe derivata non è riuscita a implementare correttamente DoClone. (Il metodo Clone crea una copia profonda corretta dell'oggetto polimorfico.)
È anche possibile contrassegnare esplicitamente il costruttore di copie sulla base, che consente lo slicing esplicito, se desiderato.
1. LA DEFINIZIONE DI PROBLEMA DI AFFETTAMENTO
Se D è una classe derivata della classe base B, è possibile assegnare un oggetto di tipo Derivato a una variabile (o parametro) di tipo Base.
ESEMPIO
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
Sebbene l'assegnazione di cui sopra sia consentita, il valore assegnato all'animale variabile perde il suo campo di razza. Questo si chiama problema di slicing .
2. COME FISSARE IL PROBLEMA DI AFFETTAMENTO
Per risolvere il problema, utilizziamo i puntatori alle variabili dinamiche.
ESEMPIO
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;
In questo caso, nessuno dei membri dei dati o delle funzioni membro della variabile dinamica a cui punta ptrD (oggetto di classe discendente) andrà perso. Inoltre, se è necessario utilizzare le funzioni, la funzione deve essere una funzione virtuale.
dog
che non fanno parte della classe Pet
(il breed
membro di dati) non vengano copiati nella variabile pet
? Il codice è interessato solo ai Pet
membri dei dati - apparentemente. Affettare è sicuramente un "problema" se è indesiderato, ma non lo vedo qui.
((Dog *)ptrP)
" Suggerisco di usarestatic_cast<Dog*>(ptrP)
Dog::breed
) non è in alcun modo un ERRORE correlato allo SLICING?
Mi sembra che il taglio non sia tanto un problema se non quando le tue classi e il tuo programma sono scarsamente progettati / progettati.
Se passo un oggetto sottoclasse come parametro a un metodo, che accetta un parametro di tipo superclasse, dovrei certamente esserne consapevole e conoscere internamente, il metodo chiamato funzionerà solo con l'oggetto superclasse (aka baseclass).
Mi sembra solo l'irragionevole aspettativa che fornire una sottoclasse in cui è richiesta una baseclass, provocherebbe in qualche modo risultati specifici per la sottoclasse, causerebbe un problema al taglio. È una progettazione scadente nell'uso del metodo o un'implementazione di una sottoclasse scadente. Immagino che di solito sia il risultato del sacrificio di un buon design OOP a favore della convenienza o delle prestazioni.
OK, ci proverò dopo aver letto molti post che spiegano lo slicing degli oggetti, ma non come diventa problematico.
Lo scenario vizioso che può provocare il danneggiamento della memoria è il seguente:
L'affettatura significa che i dati aggiunti da una sottoclasse vengono eliminati quando un oggetto della sottoclasse viene passato o restituito in base al valore o da una funzione in attesa di un oggetto della classe base.
Spiegazione: considerare la seguente dichiarazione di classe:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Poiché le funzioni di copia della baseclass non sanno nulla del derivato, viene copiata solo la parte di base del derivato. Questo è comunemente indicato come affettare.
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;
}
quando un oggetto della classe derivata viene assegnato a un oggetto della classe base, gli attributi aggiuntivi di un oggetto della classe derivata vengono separati (scartati) dall'oggetto della classe base.
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
}
Quando un oggetto classe derivata viene assegnato all'oggetto classe base, tutti i membri dell'oggetto classe derivata vengono copiati nell'oggetto classe base tranne i membri che non sono presenti nella classe base. Questi membri vengono eliminati dal compilatore. Questo si chiama Object Slicing.
Ecco un esempio:
#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;
}
Genererà:
[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'
Ho appena incontrato il problema del taglio e sono subito arrivato qui. Vorrei quindi aggiungere i miei due centesimi a questo.
Facciamo un esempio da "codice di produzione" (o qualcosa che si avvicina):
Diciamo che abbiamo qualcosa che invia azioni. Un'interfaccia utente del centro di controllo, ad esempio.
Questa interfaccia utente deve ottenere un elenco di elementi che possono essere attualmente spediti. Quindi definiamo una classe che contiene le informazioni di spedizione. Chiamiamolo Action
. Quindi Action
ha alcune variabili membro. Per semplicità ne abbiamo solo 2, essendo a std::string name
e a std::function<void()> f
. Quindi ha un oggetto void activate()
che esegue il f
membro.
Quindi l'interfaccia utente viene std::vector<Action>
fornita. Immagina alcune funzioni come:
void push_back(Action toAdd);
Ora abbiamo stabilito come appare dal punto di vista dell'interfaccia utente. Nessun problema finora. Ma un altro ragazzo che lavora a questo progetto decide improvvisamente che ci sono azioni specializzate che richiedono maggiori informazioni Action
nell'oggetto. Per quale motivo mai. Ciò potrebbe essere risolto anche con acquisizioni lambda. Questo esempio non è preso 1-1 dal codice.
Quindi il ragazzo deriva da Action
aggiungere il suo sapore.
Passa un'istanza della sua classe prodotta in casa al push_back
ma poi il programma va in tilt.
Allora, cos'è successo?
Come si potrebbe avere indovinato: l'oggetto è stato tranciato.
Le informazioni aggiuntive dall'istanza sono state perse e f
sono ora soggette a comportamenti indefiniti.
Spero che questo esempio porti luce per quelle persone che non riescono davvero a immaginare le cose quando si parla di A
s e B
s derivate in qualche modo.