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 Bma dwóch członków danych fooi bar.
Jeśli miałbyś to napisać:
B b;
A a = b;
Następnie informacje w belemencie członkowskim barzostaną utracone a.
A a = b; ajest teraz obiektem typu, Aktó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 b1do b2, ale nie masz! Skopiowaniu do udziału w b1do b2(części b1, które Bodziedziczone A), a lewy innych części b2niezmienione. b2jest teraz stworzeniem frankensteinowskim składającym się z kilku fragmentów, b1po 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 Aogó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 Ai Bskąd Bpochodzi (publicznie) A.
W tej sytuacji, C ++ pozwala przejść instancję Bdo A„s operatora przypisania (a także do konstruktora kopii). Działa to, ponieważ instancję Bmoż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 Aegzemplarz B, którego właśnie otrzymujesz. Jasne, anie będzie zawierać niektórych bczłonków, ale jak powinien? W Akoń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 b2będzie to kopia b1później. Ale niestety tak nie jest ! Jeśli go obejrzysz, odkryjesz, że b2jest stworzeniem Frankensteinowskim, zbudowanym z części b1(części, które Bdziedziczą A) i części b2(części, które tylko Bzawierają). Auć!
Co się stało? C ++ domyślnie nie traktuje operatorów przypisania jako virtual. Zatem linia a_ref = b1wywoł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_refodwołuje się do instancji B) . Teraz Aoperator przypisania oczywiście wie tylko o elementach zadeklarowanych w A, więc skopiuje tylko te, pozostawiając członków dodanych Bbez 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_castsprawdzenie, 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.
derivedwartość może być podana kodowi, że oczekuje basewartoś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ą Ai 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 wantAnAwymaga kopii derived. Jednak obiektu derivednie można skopiować całkowicie, ponieważ klasa Bmoż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 Bzostały utracone).wantAnA(jak sama nazwa wskazuje!) Chce A, to właśnie to dostaje. I instancja Abędzie zachowywać się jak A. Jak to jest zaskakujące?
derivedna 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.
Derivedmoż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.
Basemusi 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ą.
dognie należą do klasy Pet(element breeddanych) nie są kopiowane do zmiennej pet? PetNajwyraź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 Actionma pewne zmienne składowe. Dla uproszczenia mamy po prostu 2, czyli a std::string namei a std::function<void()> f. Następnie ma element, void activate()który właśnie wykonuje fczł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 Actionobiekcie. 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 Actiondodawania własnego smaku.
Podaje przykład swojej warzonej w domu klasy, push_backale 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 fsą 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 As i Bs są uzyskane w jakiś sposób.