Wróćmy do przykładu, który oglądaliśmy wcześniej:
#include <iostream>
#include <string_view>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual ~Base() = default;
virtual std::string_view getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived: public Base
{
public:
Derived(int value)
: Base{ value }
{
}
std::string_view getName() const override { return "Derived"; }
};
int main()
{
Derived derived{ 5 };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
Base& ref{ derived };
std::cout << "ref is a " << ref.getName() << " and has value " << ref.getValue() << '\n';
Base* ptr{ &derived };
std::cout << "ptr is a " << ptr->getName() << " and has value " << ptr->getValue() << '\n';
return 0;
}W powyższym przykładzie ref referencje i ptr wskazują na pochodną, która zawiera część bazową i część pochodną. Ponieważ ref i ptr są typu Base, ref i ptr mogą zobaczyć tylko podstawową część pochodnej — część pochodna nadal istnieje, ale po prostu nie można jej zobaczyć poprzez ref lub ptr. Jednak poprzez użycie funkcji wirtualnych możemy uzyskać dostęp do najbardziej pochodnej wersji funkcji. W rezultacie powyższy program wypisuje:
derived is a Derived and has value 5 ref is a Derived and has value 5 ptr is a Derived and has value 5
Ale co się stanie, jeśli zamiast ustawić odniesienie bazowe lub wskaźnik do obiektu pochodnego, po prostu przypiszemy obiekt pochodny do obiektu bazowego?
int main()
{
Derived derived{ 5 };
Base base{ derived }; // what happens here?
std::cout << "base is a " << base.getName() << " and has value " << base.getValue() << '\n';
return 0;
}Pamiętaj, że pochodna ma część bazową i część pochodną. Kiedy przypisujemy obiekt pochodny do obiektu bazowego, kopiowana jest tylko część bazowa obiektu pochodnego. Część pochodna nie jest. W powyższym przykładzie baza otrzymuje kopię części bazowej pochodnej, ale nie części pochodnej. Ta część pochodna została faktycznie „odcięta”. W związku z tym przypisanie obiektu klasy Derived do obiektu klasy Base nazywa się cięciem obiektu (lub w skrócie krojeniem).
Ponieważ baza była i nadal jest tylko bazą, wirtualny wskaźnik bazy nadal wskazuje bazę. Zatem base.getName() przekształca się w Base::getName().
Powyższy przykład wypisuje:
base is a Base and has value 5
Przy użyciu sumienia cięcie może być łagodne. Jednak niewłaściwie użyte krojenie może spowodować nieoczekiwane rezultaty na kilka różnych sposobów. Przyjrzyjmy się niektórym z tych przypadków.
Krojenie i funkcje
Możesz pomyśleć, że powyższy przykład jest nieco głupi. W końcu dlaczego miałbyś przypisać pochodną do takiej bazy? Prawdopodobnie nie. Jednak w przypadku funkcji znacznie bardziej prawdopodobne jest przypadkowe wystąpienie podziału.
Rozważ następującą funkcję:
void printName(const Base base) // note: base passed by value, not reference
{
std::cout << "I am a " << base.getName() << '\n';
}Jest to całkiem prosta funkcja z stałym parametrem obiektu bazowego, który jest przekazywany przez wartość. Jeśli wywołamy tę funkcję w następujący sposób:
int main()
{
Derived d{ 5 };
printName(d); // oops, didn't realize this was pass by value on the calling end
return 0;
}Kiedy pisałeś ten program, być może nie zauważyłeś, że baza jest parametrem wartości, a nie odniesieniem. Dlatego też, gdy zostanie wywołana jako printName(d), chociaż moglibyśmy się spodziewać, że base.getName() wywoła zwirtualizowaną funkcję getName() i wydrukuje „Jestem pochodną”, tak się nie dzieje. Zamiast tego obiekt pochodny d jest dzielony i tylko część bazowa jest kopiowana do parametru bazowego. Kiedy wykonywana jest funkcja base.getName(), mimo że funkcja getName() jest zwirtualizowana, nie ma części pochodnej klasy, do której mogłaby zostać rozwiązana. W rezultacie ten program wypisuje:
I am a BaseW tym przypadku jest całkiem oczywiste, co się stało, ale jeśli twoje funkcje w rzeczywistości nie drukują żadnych informacji identyfikujących, wyśledzenie błędu może być trudne.
Oczywiście można łatwo uniknąć dzielenia w tym miejscu, czyniąc parametr funkcji referencją zamiast przekazywanej wartości (jeszcze kolejny powód, dla którego przekazywanie klas przez referencję zamiast wartości jest dobrym pomysłem).
void printName(const Base& base) // note: base now passed by reference
{
std::cout << "I am a " << base.getName() << '\n';
}
int main()
{
Derived d{ 5 };
printName(d);
return 0;
}Wypisuje:
I am a Derived
Cięcie na plasterki wektory
Kolejnym obszarem, w którym nowi programiści napotykają problemy z krojeniem, są próby zaimplementowania polimorfizmu za pomocą std::vector. Rozważmy następujący program:
#include <vector>
int main()
{
std::vector<Base> v{};
v.push_back(Base{ 5 }); // add a Base object to our vector
v.push_back(Derived{ 6 }); // add a Derived object to our vector
// Print out all of the elements in our vector
for (const auto& element : v)
std::cout << "I am a " << element.getName() << " with value " << element.getValue() << '\n';
return 0;
}Ten program kompiluje się dobrze. Ale po uruchomieniu wypisuje:
I am a Base with value 5 I am a Base with value 6
Podobnie jak w poprzednich przykładach, ponieważ std::vector został zadeklarowany jako wektor typu Base, gdy do wektora dodano Derived(6), został on pocięty.
Naprawienie tego jest trochę trudniejsze. Wielu nowych programistów próbuje utworzyć std::vector odwołań do obiektu, na przykład:
std::vector<Base&> v{};Niestety, to się nie skompiluje. Elementy std::vector muszą być przypisywane, podczas gdy odwołań nie można ponownie przypisywać (tylko inicjować).
Jednym ze sposobów rozwiązania tego problemu jest utworzenie wektora wskaźników:
#include <iostream>
#include <vector>
int main()
{
std::vector<Base*> v{};
Base b{ 5 }; // b and d can't be anonymous objects
Derived d{ 6 };
v.push_back(&b); // add a Base object to our vector
v.push_back(&d); // add a Derived object to our vector
// Print out all of the elements in our vector
for (const auto* element : v)
std::cout << "I am a " << element->getName() << " with value " << element->getValue() << '\n';
return 0;
}Wypisuje:
I am a Base with value 5 I am a Derived with value 6
co działa! Kilka komentarzy na ten temat. Po pierwsze, nullptr jest teraz prawidłową opcją, która może być pożądana lub nie. Po drugie, musisz teraz zająć się semantyką wskaźników, co może być niewygodne. Ale zaletą jest to, że używanie wskaźników pozwala nam umieszczać w wektorze dynamicznie przydzielane obiekty (tylko nie zapomnij ich jawnie usunąć).
Inną opcją jest użycie std::reference_wrapper, który jest klasą imitującą odwołanie z możliwością ponownego przypisania:
#include <functional> // for std::reference_wrapper
#include <iostream>
#include <string_view>
#include <vector>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual ~Base() = default;
virtual std::string_view getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived : public Base
{
public:
Derived(int value)
: Base{ value }
{
}
std::string_view getName() const override { return "Derived"; }
};
int main()
{
std::vector<std::reference_wrapper<Base>> v{}; // a vector of reassignable references to Base
Base b{ 5 }; // b and d can't be anonymous objects
Derived d{ 6 };
v.push_back(b); // add a Base object to our vector
v.push_back(d); // add a Derived object to our vector
// Print out all of the elements in our vector
// we use .get() to get our element out of the std::reference_wrapper
for (const auto& element : v) // element has type const std::reference_wrapper<Base>&
std::cout << "I am a " << element.get().getName() << " with value " << element.get().getValue() << '\n';
return 0;
}Frankenobject
W powyższych przykładach widzieliśmy przypadki, w których cięcie prowadziło do błędnego wyniku, ponieważ klasa pochodna miała został odcięty. Przyjrzyjmy się teraz innemu niebezpiecznemu przypadkowi, w którym obiekt pochodny nadal istnieje!
Rozważ następujący kod:
int main()
{
Derived d1{ 5 };
Derived d2{ 6 };
Base& b{ d2 };
b = d1; // this line is problematic
return 0;
}Pierwsze trzy linie funkcji są całkiem proste. Utwórz dwa obiekty pochodne i ustaw odniesienie bazowe do drugiego.
Czwarta linia pokazuje, gdzie coś idzie nie tak. Ponieważ b wskazuje na d2 i przypisujemy d1 b, można by pomyśleć, że w rezultacie d1 zostanie skopiowane do d2 - i tak by się stało, gdyby b było pochodną. Ale b jest bazą, a operator=, który C++ zapewnia dla klas, domyślnie nie jest wirtualny. W rezultacie wywoływany jest operator przypisania, który kopiuje bazę, a do d2 kopiowana jest tylko podstawowa część d1.
W rezultacie odkryjesz, że d2 ma teraz podstawową część d1 i pochodną część d2. W tym konkretnym przykładzie nie stanowi to problemu (ponieważ klasa Derived nie ma własnych danych), ale w większości przypadków właśnie utworzysz obiekt Frankenobject — złożony z części wielu obiektów.
Co gorsza, nie ma łatwego sposobu, aby temu zapobiec (poza unikaniem w miarę możliwości takich przypisań).
Wskazówka
Jeśli klasa Base nie jest zaprojektowana tak, aby mogła być tworzona samodzielnie (np. jest to po prostu klasy interfejsu), można uniknąć dzielenia na plasterki, uniemożliwiając kopiowanie klasy Base (poprzez usunięcie konstruktora kopiującego Base i operatora przypisania Base).
Wnioski
Chociaż C++ obsługuje przypisywanie obiektów pochodnych do obiektów bazowych poprzez cięcie obiektów, ogólnie rzecz biorąc, może to powodować jedynie bóle głowy i ogólnie rzecz biorąc, należy starać się unikać krojenia. Upewnij się, że parametry funkcji są referencjami (lub wskaźnikami) i staraj się unikać wszelkiego rodzaju przekazywania wartości w przypadku klas pochodnych.

