• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Pytanie - rzutowanie obiektów

Object Storage Arubacloud
0 głosów
631 wizyt
pytanie zadane 22 czerwca 2018 w C i C++ przez fruczka Użytkownik (570 p.)
Cześć. Czy jest możliwość rzutowania obiektu klasy na obiekt klasy bazowej i klasy pochodnej? I w jednym zdanku, żeby takie coś uzasadnić jeśli mogę poprosić.

2 odpowiedzi

+1 głos
odpowiedź 22 czerwca 2018 przez Szfierzak Gaduła (3,750 p.)
Na klasę bazową nie trzeba rzutować. Dziedziczenie sprawia, że możesz posługiwać się np. wskaźnikiem na klasę bazową, przechowując obiekt klasy pochodnej. W drugą stronę, czyli z bazowej na pochodną, jest możliwe rzutowanie, ale z przeciążeniem operatora rzutowania. Jednak w przypadku próby rzutowania z bazowej na pochodną to klasa pochodna rozszerza klasę bazową, więc polecam raczej stworzyć nowy obiekt klasy pochodnej na podstawie obiektu klasy bazowej. Nawet rzutowanie między jedną klasą a drugą, które nie są ze sobą związane, jest możliwe, tylko trzeba przeciążyć operatory rzutowania, żeby zadbać o "logikę" takiego rzutowania.
komentarz 22 czerwca 2018 przez mokrowski Mędrzec (155,460 p.)

@Szfierzak, możesz wyjaśnić co oznacza "przeciążyć operator rzutowania"?

komentarz 22 czerwca 2018 przez Szfierzak Gaduła (3,750 p.)

Zadaniem przeciążonego operatora rzutowania jest zamiana obiektu klasy A na obiekt klasy B. W dużym uproszczeniu:

class KlasaB
{
...
}

class KlasaA
{
...
  operator KlasaB () { return obiekt_klasy_B; }
};
 
 
KlasaA obiektA();
KlasaB obiektB = obiektA; // tu wykona się ten operator

 

komentarz 22 czerwca 2018 przez mokrowski Mędrzec (155,460 p.)
Mhm... Czyli dziedziczeniem "spawasz kod" w dół (co jest naturalne dla tej relacji) a operatorem konwersji (pod tą nazwą je znam) "spawasz w górę" równocześnie łamiąc regułę Liskov.

IMHO lepiej zostawić takie rzutowanie jawne...
komentarz 23 czerwca 2018 przez Szfierzak Gaduła (3,750 p.)

Nie rozumiem za bardzo co masz na myśli mówiąc "spawasz kod"? Niestety nie rozumiem tego pojęcia, podobnie jak Ty nie rozumiałeś pojęcia rzutowania, które jest chyba dość powszechnie używanym spolszczeniem konwersji. Jednak intuicja podpowiada mi, że dziedziczenie chyba właśnie do tego służy, żeby korzystać z funkcjonalności napisanych dla klas bazowych, podczas używania klas pochodnych, w sposób jaki to przedstawiłem wcześniej.

Jeżeli chodzi o "spawanie w dół", to pytanie było, czy taka konwersja jest możliwa, odpowiedź - tak, jest możliwa. Nie rozumiem, dlaczego łamię w ten sposób regułę Liskov, nie znam zastosowania, nie widzę kodu, nie mogę stwierdzić czy ta regułą jest łamana czy nie. Nie wiem też, czy tworzenie obiektu klasy pochodnej na podstawie klasy bazowej jest złe.

Zastosowanie rzutowania/konwersji jawnej nie zwalnia nas z implementacji operatora konwersji.

Różnego rodzaju projekty na studiach często wymuszają użycie jakiegoś mechanizmu, dla pokazania samego działania. Już tworząc pusty projekt C++ np. w C::B wita nas w nim 

using namespace std;

a książki niektórzy mogliby pisać dlaczego nie używać, kiedy używać i jakie z tego są korzyści i jakie zagrożenia, o których dowiadujemy się na dalszym etapie nauki i/lub w pracy zawodowej. 

2
komentarz 23 czerwca 2018 przez mokrowski Mędrzec (155,460 p.)
edycja 23 czerwca 2018 przez mokrowski

Ok... Po kolei. Zapytałem o "operator rzutowania" bo nie spotkałem się z takim nazewnictwem. Tylko tyle. Słyszałem o funkcjach konwersji lub operatorze konwersji. To jest kwestia tłumaczenia i także z tego powodu nie polegam na takich tłumaczeniach bo wywołują niejasności. Np. cytujący standard języka C++ fragment witryny: https://en.cppreference.com/w/cpp/language/explicit_cast czy https://en.cppreference.com/w/cpp/language/cast_operator . Sam standard tego pojęcia także nie zawiera. No ale jak napisałem.. winę ponoszą tłumacze i pośrednio ja bo nie sięgam do źródeł "po translacji" więc mogłem nie wiedzieć (choć w tym przypadku ponownie przekonuję się że nie powinienem).

Relacja dziedziczenia jest naturalnym spajaniem kodu. Tak się składa że także często i ... toksycznym. O "spawaniu" napisałem nie bez kozery bo to określenie obrazuje późniejsze problemy związane z separacją kodu. Aby nie powtarzać i nie tłumaczyć:

https://en.wikipedia.org/wiki/Coupling_(computer_programming)

https://en.wikipedia.org/wiki/Cohesion_(computer_science)

No ale w kierunku rodzić -> dziecko, to normalna rzecz. Nikt o nic nie ma tu pretensji tym bardziej że C++ inaczej nie implementuje typowych interfejsów. Trzeba tylko zdawać sobie sprawę z dodatkowych konsekwencji i tyle.

Rzutując jednak w górę drzewa dziedziczenia łamiesz regułę Liskov https://en.wikipedia.org/wiki/Liskov_substitution_principle  (na marginesie radzę także zerknąć do S.O.L.I.D.) a dodając jak pisałeś "operator rzutowania", czynisz to niejawnie. To jest szkodliwe i lepiej pokazać taką operację poprzez xxx_cast<Na_Co> niż dokonując tego poprzez konwersję (tu się upieram.. to lepsza nazwa).

Dodatkowo, łączysz w sposób nieuprawniony implementację klasy pochodnej z bazową czyniąc ich przyszłe oddzielenie (w zasadzie) już totalnie niemożliwym. Z tego powodu obrazowo nazwałem to "spawaniem". 

I tu dochodzę do sedna. Uczono mnie (oraz przekonałem się na własnej skórze w większych projektach) że operator konwersji nie służy do rzutowania do klasy bazowej a powody już podałem. Uczono mnie (i jak poprzednio .. zawodowo się przekonałem) że do rzutowania służą wyrażenia XXX_cast<YYY> które jawnie pokazują w jaki sposób i w jakim zakresie łamana jest hierarchia typów (problem z Liskov to tylko jeden z problemów) a operator konwersji służy do ... konwersji :)

Nie jestem po prostu przekonany co do tego co napisałeś.

+1 głos
odpowiedź 23 czerwca 2018 przez mokrowski Mędrzec (155,460 p.)

Ogólna odpowiedź co do rzutowania brzmi:

1. Z klasy pochodnej do bazowej z użyciem static_cast<Bazowa>.

2. Z klasy bazowej na pochodną nie można (*)

Być może zaskakujące jest to 2. Na gruncie teorii OOD (ang. Object Oriented Design), łatwe rzutowanie nie powinno się powieść bo typ ogólny rzutowany jest na specjalizację a dla specjalizacji jest on.. zbyt ogólny :) Ale wyjaśnia się to także jeśli zerknąć na rozmiar obydwu klas.

#include <iostream>

struct Base {
    Base(int value1, double value2)
        : value1{value1}, value2{value2} {}
    void getInfo() const {
        std::cout << "value1 = " << value1 << " value2 = " << value2;
    }
private:
    int value1;
    double value2;
};

struct Derived: public Base {
    Derived(int value1, double value2, double value3)
        : Base(value1, value2), value3{value3} {}
    void getInfo() const {
        Base::getInfo();
        std::cout << " value3 = " << value3;
    }
private:
    double value3;
};

int main() {
    Base bs1 = static_cast<Base>(Derived(1, 2.3, 3.55));
    bs1.getInfo();
    std::cout << '\n';
    // Derived dr1 = ???_???<Derived>(Base(1, 2.3));
    std::cout << "Base size = " << sizeof(bs1) << '\n'
        << "Derived size = " << sizeof(Derived) << '\n';
}

Jak widzisz, klasa pochodna może być (i w mojej implementacji jest) większa lub .. taka sama (o ile nie dochodzą nowe atrybuty). Stąd nie da się jej "upchnąć" w przestrzeni pamięci przeznaczonej na klasę bazową bez straty. Zachowuje się jak klasa bazowa bo została "przycięta" do rozmiaru bazowej.

Klasy bazowej w przestrzeni pamięci klasy pochodnej z kolei nie da się umieścić. W przeciwnym wypadku powstaje wiele pytań. Np. pytanie jak ma zachować dana metoda z klasy bazowej. Ma działać jak pochodna czy może jak bazowa? Jak pochodna nie może bo nie ma kompletnych danych (ma dane bazowej a nie ma pochodnej). Jak bazowa nie może bo ... ma być pochodną... A co zrobić z metodami obecnymi w klasie pochodnej jeśli były by dodatkowe? Przecież jeśli pozwolić na rzutowanie to.. nie było by ich! Lepiej więc nie pozwolić i tyle.

(*) pomijam tu fakt brutalnych rozwiązań z udowodnieniem że się da poprzez wskaźnik...

Derived dr1 = *(reinterpret_cast<Derived*>(static_cast<void *>(&bs1)));

...nie sądzę żeby to było kształcące a i do wskaźników dojdę.

Nie czas i miejsce także na to by wyciągać jakieś wnioski z teorii typów czy modelu ułożenia klasy w pamięci. Jeśli chcesz tu polecę literaturę.

Trochę inaczej jest jeśli decydujesz się na wskaźniki lub referencje:

1. Ze wskaźnika klasy pochodnej do klasy bazowej

2. Ze wskaźnika klasy bazowej do klasy pochodnej z użyciem static_cast

#include <iostream>

struct Base {
    Base(int value1, double value2): value1{value1}, value2{value2} {}
    void getInfo() const {
        std::cout << "value1 = " << value1 << " value2 = " << value2;
    }
private:
    int value1;
    double value2;
};

struct Derived: public Base {
    Derived(int value1, double value2, double value3)
        : Base(value1, value2), value3{value3} {}
    void getInfo() const {
        Base::getInfo();
        std::cout << " value3 = " << value3;
    }
private:
    double value3;
};

int main() {
    Derived dr1{1, 2.3, 3.55};

    Base * bs1Ptr = &dr1;
    bs1Ptr->getInfo();
    std::cout << '\n';

    Base bs1{1, 2.3};

    Derived * dr1Ptr = static_cast<Derived *>(&bs1);
    dr1Ptr->getInfo();
    std::cout << '\n';
}

Proste jeśli by rozważyć informację czym jest wskaźnik. Z tego powodu kompilator nie ma pytań jeśli chodzi o rzutowanie ze wskaźnika pochodnej na wskaźnik bazowej. Także spowoduje to że wołany będzie kontekst metod z klasy bazowej a nie pochodnej! No ale ... chciałeś!

W przypadku wskaźnika z pochodnej na bazową znów jest problem. Tu jest niebezpieczeństwo bo nie wiadomo jaką wartość ma value3 z klasy pochodnej więc ma ... śmieci bo ten atrybut nie był inicjowany. No ale metoda jest z klasy pochodnej a nie ze źródła rzutowania (bazowej). Więc jest prawie .. prawie... :/ Tu także pytanie (do samodzielnego znalezienia) skąd i czy zawsze można być pewnym że metoda będzie z klasy pochodnej a nie z bazowej.

Pozostaje więc pytanie co zrobić jeśli absolutnie jesteś pewien że chcesz mieć takie rzutowania.... wszystkie! Można decydować się na konwersje. Te można zrobić na kilka sposobów:

1. Dokonując implementacji odpowiednich konstruktorów.

2. Dokonując implementacji funkcji (lub metod) konwersji.

3. Jeszcze inne... 

Była luka z bazowej na pochodną. Teraz ją "załatam".

Poprzez konstruktor kopiujący czyli "u źródła"...

#include <iostream>

struct Base {
    Base(int value1, double value2): value1{value1}, value2{value2} {}
    void getInfo() const {
        std::cout << "value1 = " << value1 << " value2 = " << value2;
    }
private:
    int value1;
    double value2;
};

struct Derived: public Base {
    Derived(int value1, double value2, double value3)
        : Base(value1, value2), value3{value3} {}
    // Tu konwersja poprzez konstruktor kopiujący
    Derived(const Base& src)
        : Base(src), value3{} {}
    void getInfo() const {
        Base::getInfo();
        std::cout << " value3 = " << value3;
    }
private:
    double value3;
};


int main() {
    Derived dr1 = Base(1, 2.3);
    dr1.getInfo();
    std::cout << '\n';

Przez funkcję konwersji "czyli u celu"...

#include <iostream>

struct Derived;

struct Base {
    Base(int value1, double value2);
    // Tu konwersja poprzez operator konwersji
    operator Derived();
    void getInfo() const;
private:
    int value1;
    double value2;
};

struct Derived: public Base {
    Derived(int value1, double value2, double value3);
    void getInfo() const;
private:
    double value3;
};

Base::Base(int value1, double value2): value1{value1}, value2{value2} {}

// I tenże... 
Base::operator Derived() {
    return Derived(value1, value2, 0);
}

void Base::getInfo() const {
    std::cout << "value1 = " << value1 << " value2 = " << value2;
}

Derived::Derived(int value1, double value2, double value3)
    : Base(value1, value2), value3{value3} {}

void Derived::getInfo() const {
    Base::getInfo();
    std::cout << " value3 = " << value3;
}

int main() {
    Derived dr1 = Base(1, 2.3);
    dr1.getInfo();
    std::cout << '\n';
}

Niezła ekwilibrystyka z implementacjami... Już samo to intuicyjnie wskazuje że coś jest robione "pod włos".

Takie działanie można osiągnąć także z użyciem innych metod (zaprzyjaźniania, semantyki przenoszenia...) 

Radzę także doczytać czy i co z tego co tu napisałem jest UB a co nie jest :)

Z kolei wskaźniki (czy referencje) i metody wirtualne oraz polimorfizm, to zupełnie inne zagadnienie. Tu było pytanie o rzutowanie a nie zachowania polimorficzne.

Podtrzymuję także to co napisałem w komentarzu. Konwersja w górę relacji dziedziczenia (czyli generalizacja) jest patologiczna i prędzej czy później będzie się mściła.

Nie odpowiedziałem także w pełni bo i pytanie było ogólne. No i nie w jednym zdaniu... :)

Podobne pytania

0 głosów
2 odpowiedzi 377 wizyt
+1 głos
1 odpowiedź 2,563 wizyt
0 głosów
0 odpowiedzi 137 wizyt
pytanie zadane 28 grudnia 2022 w PHP przez Filip384 Nowicjusz (120 p.)

92,576 zapytań

141,426 odpowiedzi

319,652 komentarzy

61,961 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...