Ktoś wspomniał o tym w IRC jako problem krojenia.
Ktoś wspomniał o tym w IRC jako problem krojenia.
Odpowiedzi:
„Krojenie” polega na przypisywaniu obiektu klasy pochodnej do instancji klasy bazowej, co powoduje utratę części informacji - część z nich jest „krojona”.
Na przykład,
class A {
int foo;
};
class B : public A {
int bar;
};
Zatem obiekt typu B
ma dwóch członków danych foo
i bar
.
Jeśli miałbyś to napisać:
B b;
A a = b;
Następnie informacje w b
elemencie członkowskim bar
zostaną utracone a
.
A a = b;
a
jest teraz obiektem typu, A
który ma kopię B::foo
. Myślę, że błędem będzie teraz go odrzucić.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Może uważasz, że zostały skopiowane b1
do b2
, ale nie masz! Skopiowaniu do udziału w b1
do b2
(części b1
, które B
odziedziczone A
), a lewy innych części b2
niezmienione. b2
jest teraz stworzeniem frankensteinowskim składającym się z kilku fragmentów, b1
po których następują fragmenty b2
. Ugh! Głosowanie w dół, ponieważ myślę, że odpowiedź jest bardzo myląca.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
„ Prawdziwy problem występuje, jeśli ” ... wywodzisz się z klasy z nie-wirtualnym operatorem przypisania. Czy w A
ogóle jest przeznaczony do uzyskania Nie ma żadnych funkcji wirtualnych. Jeśli wywodzisz się z typu, musisz poradzić sobie z tym, że można wywoływać jego funkcje składowe!
Większość odpowiedzi tutaj nie wyjaśnia, na czym polega faktyczny problem krojenia. Wyjaśniają tylko łagodne przypadki krojenia, a nie zdradzieckie. Załóżmy, podobnie jak inne odpowiedzi, że masz do czynienia z dwiema klasami A
i B
skąd B
pochodzi (publicznie) A
.
W tej sytuacji, C ++ pozwala przejść instancję B
do A
„s operatora przypisania (a także do konstruktora kopii). Działa to, ponieważ instancję B
można przekształcić w a const A&
, czego oczekują operatorzy przypisania i konstruktory kopiowania.
B b;
A a = b;
Nie dzieje się tam nic złego - poprosiłeś o A
egzemplarz B
, którego właśnie otrzymujesz. Jasne, a
nie będzie zawierać niektórych b
członków, ale jak powinien? W A
końcu to nie jest B
, więc nawet nie słyszało o tych członkach, nie mówiąc już o ich przechowywaniu.
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Możesz pomyśleć, że b2
będzie to kopia b1
później. Ale niestety tak nie jest ! Jeśli go obejrzysz, odkryjesz, że b2
jest stworzeniem Frankensteinowskim, zbudowanym z części b1
(części, które B
dziedziczą A
) i części b2
(części, które tylko B
zawierają). Auć!
Co się stało? C ++ domyślnie nie traktuje operatorów przypisania jako virtual
. Zatem linia a_ref = b1
wywoła operatora przypisania A
, a nie operatora B
. Wynika to z tego, że w przypadku funkcji nie wirtualnych zadeklarowany (formalnie: statyczny ) typ (który jest A&
) określa, która funkcja jest wywoływana, w przeciwieństwie do faktycznego (formalnie: dynamicznego ) typu (który byłby B
, ponieważ a_ref
odwołuje się do instancji B
) . Teraz A
operator przypisania oczywiście wie tylko o elementach zadeklarowanych w A
, więc skopiuje tylko te, pozostawiając członków dodanych B
bez zmian.
Przypisywanie tylko do części obiektu zwykle nie ma sensu, ale C ++ niestety nie zapewnia wbudowanego sposobu, aby tego zabronić. Możesz jednak rzucić własne. Pierwszym krokiem jest uczynienie operatora przypisania wirtualnym . Zagwarantuje to, że wywoływany jest zawsze operator przypisania typu rzeczywistego , a nie typu deklarowanego . Drugim krokiem jest dynamic_cast
sprawdzenie, czy przypisany obiekt ma zgodny typ. Trzecim krokiem jest zrobić rzeczywiste zadanie w członie (chronione!) assign()
, Ponieważ B
„s assign()
będzie prawdopodobnie chcesz użyć A
” s assign()
skopiować A
„s, członkowie.
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
}
};
Należy zauważyć, że dla czystej wygody, B
„s operator=
covariantly nadpisuje typ zwracany, ponieważ wie, że to powrót instancję B
.
derived
wartość może być podana kodowi, że oczekuje base
wartości, albo dowolne pochodne odniesienie może być użyte jako odniesienie podstawowe. Chciałbym zobaczyć język z systemem czcionek, który osobno odnosi się do obu pojęć. Istnieje wiele przypadków, w których referencyjne pochodne powinny być podstawialne dla referencyjnych baz, ale instancje pochodne nie powinny zastępować bazowych; istnieje również wiele przypadków, w których instancje powinny być konwertowalne, ale odwołania nie powinny zastępować.
Jeśli masz klasę podstawową A
i pochodną B
, możesz wykonać następujące czynności.
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
Teraz metoda wantAnA
wymaga kopii derived
. Jednak obiektu derived
nie można skopiować całkowicie, ponieważ klasa B
może wymyślić dodatkowe zmienne składowe, które nie należą do jego klasy podstawowej A
.
Dlatego, aby wywołać wantAnA
, kompilator „odcina” wszystkie dodatkowe elementy klasy pochodnej. Rezultatem może być obiekt, którego nie chcesz utworzyć, ponieważ
A
-object (wszystkie specjalne zachowania klasy B
zostały utracone).wantAnA
(jak sama nazwa wskazuje!) Chce A
, to właśnie to dostaje. I instancja A
będzie zachowywać się jak A
. Jak to jest zaskakujące?
derived
na typ A
. Niejawne rzutowanie jest zawsze źródłem nieoczekiwanego zachowania w C ++, ponieważ często trudno jest zrozumieć na podstawie lokalnego kodu, że miało miejsce rzutowanie.
To są wszystkie dobre odpowiedzi. Chciałbym tylko dodać przykład wykonania przy przekazywaniu obiektów według wartości vs przez referencję:
#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());
}
Dane wyjściowe to:
Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A'
Trzecie dopasowanie w Google dla „C ++ slicing” daje mi ten artykuł w Wikipedii http://en.wikipedia.org/wiki/Object_slicing i ten (nagrzany, ale kilka pierwszych postów określa problem): http://bytes.com/ forum / thread163565.html
Tak więc, kiedy przypisujesz obiekt podklasy do superklasy. Nadklasa nic nie wie o dodatkowych informacjach w podklasie i nie ma miejsca na ich przechowywanie, więc dodatkowe informacje zostają „odcięte”.
Jeśli te linki nie dostarczają wystarczających informacji do „dobrej odpowiedzi”, edytuj swoje pytanie, aby dać nam znać, czego więcej szukasz.
Problem krojenia jest poważny, ponieważ może powodować uszkodzenie pamięci i bardzo trudno jest zagwarantować, że program go nie dotknie. Aby zaprojektować go z języka, klasy obsługujące dziedziczenie powinny być dostępne tylko przez odniesienie (a nie przez wartość). Język programowania D ma tę właściwość.
Rozważ klasę A i klasę B wywodzącą się z A. Zepsucie pamięci może się zdarzyć, jeśli część A ma wskaźnik p i instancję B wskazującą p na dodatkowe dane B. Następnie, gdy dodatkowe dane zostają odcięte, p wskazuje na śmieci.
Derived
można je domyślnie przekonwertować na Base
.) Jest to oczywiście sprzeczne z zasadą otwartego zamknięcia i dużym obciążeniem konserwacyjnym.
W C ++ obiekt klasy pochodnej można przypisać do obiektu klasy bazowej, ale inny sposób nie jest możliwy.
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
}
Wycinanie obiektów ma miejsce, gdy obiekt klasy pochodnej jest przypisany do obiektu klasy bazowej, dodatkowe atrybuty obiektu klasy pochodnej są odcinane w celu utworzenia obiektu klasy bazowej.
Problem krojenia w C ++ wynika z semantyki wartości jego obiektów, która pozostała głównie ze względu na kompatybilność ze strukturami C. Musisz użyć jawnej odwołania lub składni wskaźnika, aby osiągnąć „normalne” zachowanie obiektu występujące w większości innych języków, które wykonują obiekty, tj. Obiekty są zawsze przekazywane przez odniesienie.
Krótkie odpowiedzi są takie, że pocinasz obiekt przez przypisanie obiektu pochodnego do obiektu bazowego według wartości , tzn. Pozostały obiekt jest tylko częścią obiektu pochodnego. Aby zachować semantykę wartości, krojenie jest rozsądnym zachowaniem i ma swoje stosunkowo rzadkie zastosowania, które nie istnieją w większości innych języków. Niektórzy uważają, że jest to cecha C ++, podczas gdy wielu uważało ją za jedną z dziwactw / błędów w C ++.
struct
, kompatybilnością lub innym brakiem wyczucia, jak powiedział ci dowolny przypadkowy ksiądz OOP.
Base
musi zająć dokładnie sizeof(Base)
bajty w pamięci, z możliwym wyrównaniem, być może dlatego „przypisanie” (na stosie ) nie kopiuje pochodnych członków klasy, ich przesunięcia są poza rozmiarem. Aby uniknąć „utraty danych”, po prostu używaj wskaźnika, jak każdy inny, ponieważ pamięć wskaźnika jest ustalona na miejscu i ma rozmiar, a stos jest bardzo zmienny
Więc ... Dlaczego utrata uzyskanych informacji jest zła? ... ponieważ autor klasy pochodnej mógł zmienić reprezentację tak, że odcięcie dodatkowych informacji zmienia wartość reprezentowaną przez obiekt. Może się to zdarzyć, jeśli klasa pochodna zostanie użyta do buforowania reprezentacji, która jest bardziej wydajna dla niektórych operacji, ale kosztowna jest jej powrót do reprezentacji podstawowej.
Pomyślałem również, że ktoś powinien również wspomnieć o tym, co należy zrobić, aby uniknąć krojenia ... Uzyskaj kopię Standardów kodowania C ++, wytycznych 101 zasad i najlepszych praktyk. Radzenie sobie z krojeniem to # 54.
Sugeruje to nieco wyrafinowany wzorzec, aby w pełni poradzić sobie z tym problemem: mieć chroniony konstruktor kopii, chroniony czysty wirtualny DoClone i publiczny klon z aser, który powie ci, czy (dodatkowa) klasa pochodna nie zaimplementowała poprawnie DoClone. (Metoda klonowania tworzy odpowiednią głęboką kopię obiektu polimorficznego.)
Możesz także zaznaczyć konstruktora kopiowania w bazie jako jawny, co pozwala na wyraźne krojenie, jeśli jest to pożądane.
1. DEFINICJA PROBLEMU KROJENIA
Jeśli D jest klasą pochodną klasy bazowej B, wówczas można przypisać obiekt typu Derived do zmiennej (lub parametru) typu Base.
PRZYKŁAD
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
Chociaż powyższe przypisanie jest dozwolone, wartość przypisana zmiennemu zwierzakowi traci pole rasy. Nazywa się to problemem krojenia .
2. JAK NAPRAWIĆ PROBLEM Z KROJENIEM
Aby pokonać problem, używamy wskaźników do zmiennych dynamicznych.
PRZYKŁAD
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;
W takim przypadku żaden element danych lub funkcja elementu zmiennej dynamicznej wskazywanej przez ptrD (obiekt klasy potomnej) nie zostanie utracony. Ponadto, jeśli chcesz użyć funkcji, funkcja musi być funkcją wirtualną.
dog
nie należą do klasy Pet
(element breed
danych) nie są kopiowane do zmiennej pet
? Pet
Najwyraźniej kod jest zainteresowany tylko elementami danych. Krojenie jest zdecydowanie „problemem”, jeśli jest niepożądane, ale nie widzę tego tutaj.
((Dog *)ptrP)
” Sugeruję użyciestatic_cast<Dog*>(ptrP)
Dog::breed
), nie jest w żaden sposób BŁĄD związany z SLICINGEM?
Wydaje mi się, że krojenie nie jest tak dużym problemem, jak wtedy, gdy twoje własne klasy i program są źle zaprojektowane / zaprojektowane.
Jeśli przekażę obiekt podklasy jako parametr metodzie, która przyjmuje parametr typu superklasa, z pewnością powinienem o tym wiedzieć i wiedzieć wewnętrznie, wywoływana metoda będzie działać tylko z obiektem nadklasy (aka klasa podstawowa).
Wydaje mi się, że jedynie nierozsądne oczekiwanie, że zapewnienie podklasy, w której żądana jest klasa podstawowa, w jakiś sposób doprowadziłoby do określonych wyników podklasy, spowodowałoby problem z krojeniem. Jest to albo zły projekt w użyciu metody, albo słaba implementacja podklasy. Domyślam się, że zwykle jest to wynikiem poświęcenia dobrego projektu OOP na rzecz korzyści lub wzrostu wydajności.
OK, spróbuję po przeczytaniu wielu postów wyjaśniających krojenie obiektów, ale nie w jaki sposób staje się to problematyczne.
Zły scenariusz, który może spowodować uszkodzenie pamięci, jest następujący:
Wycinanie oznacza, że dane dodane przez podklasę są odrzucane, gdy obiekt tej podklasy jest przekazywany lub zwracany przez wartość lub z funkcji oczekującej obiektu klasy podstawowej.
Objaśnienie: Rozważ następującą deklarację klasy:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Ponieważ funkcje kopiowania klasy podstawowej nie wiedzą nic o pochodnej, kopiowana jest tylko podstawowa część pochodnej. Jest to powszechnie określane jako krojenie.
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;
}
gdy obiekt klasy pochodnej jest przypisany do obiektu klasy bazowej, dodatkowe atrybuty obiektu klasy pochodnej są odcinane (odrzucane) z obiektu klasy bazowej.
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
}
Gdy obiekt klasy pochodnej jest przypisany do obiektu klasy bazowej, wszystkie elementy obiektu klasy pochodnej są kopiowane do obiektu klasy bazowej, z wyjątkiem elementów, które nie są obecne w klasie bazowej. Te elementy są usuwane przez kompilator. Nazywa się to Wycinaniem obiektów.
Oto przykład:
#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;
}
Wygeneruje:
[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'
Właśnie natknąłem się na problem krojenia i natychmiast wylądowałem tutaj. Pozwólcie, że dodam do tego moje dwa centy.
Weźmy przykład z „kodu produkcyjnego” (lub czegoś, co zbliża się trochę):
Powiedzmy, że mamy coś, co wywołuje działania. Na przykład interfejs centrum sterowania.
Ten interfejs użytkownika musi uzyskać listę rzeczy, które można obecnie wysłać. Definiujemy więc klasę, która zawiera informacje o wysyłce. Nazwijmy to Action
. A więc Action
ma pewne zmienne składowe. Dla uproszczenia mamy po prostu 2, czyli a std::string name
i a std::function<void()> f
. Następnie ma element, void activate()
który właśnie wykonuje f
członka.
Tak więc interfejs użytkownika jest std::vector<Action>
dostarczany. Wyobraź sobie niektóre funkcje, takie jak:
void push_back(Action toAdd);
Teraz ustaliliśmy, jak to wygląda z punktu widzenia interfejsu użytkownika. Jak dotąd żaden problem. Ale jakiś inny facet, który pracuje nad tym projektem, nagle decyduje, że istnieją specjalne działania, które wymagają więcej informacji w Action
obiekcie. Z jakiego powodu kiedykolwiek. Można to również rozwiązać za pomocą ujęć lambda. Ten przykład nie jest wzięty z kodu 1-1.
Więc facet wywodzi się z Action
dodawania własnego smaku.
Podaje przykład swojej warzonej w domu klasy, push_back
ale potem program oszalał.
Więc co się stało?
Jak można się domyślić: obiekt został plasterkach.
Dodatkowe informacje z instancji zostały utracone i f
są teraz podatne na niezdefiniowane zachowanie.
Mam nadzieję, że ten przykład daje światło o dla tych ludzi, którzy nie mogą sobie wyobrazić rzeczy, gdy mówimy o A
s i B
s są uzyskane w jakiś sposób.