Funkcje wirtualne - automatyczny ich wybór nieprawidłowy

0

Witam,

Od razu tytułem wstępu zamieszczam link do aktualnej wersji projektu:
http://www.sendspace.pl/file/b24934630d71df94def42ec

Opis zgrubsza projektu:

Mamy klasę abstrakcyjną Figure, po której dziedziczą: Square (kwadrat), Rectangle (prostokąt), Rhombus (romb) i Quadrilateral (czworokąt). Elementami składowymi Figure są cztery punkty(wierzchołki) (które, od razu mówię, że z niepodważalnych przyczyn nie mogą być w tablicy, jakby kto się krzywił na switche w kodzie), oraz jej nazwa. Poza tym są funkcje czysto wirtualne: Area (pole) oraz Perimeter (obwód), oraz funkcja SayHello która wypisuje nazwę. Ponadto jest jeszcze color, ale wgl z tego pola póki co nie korzystam.

Poza konstruktorami bezparametrowym oraz kopiującym, każda z 4 figur posiada odpowiednio takie konstruktory:
Quadrilateral - inicjalizowany 4ma punktami
Rhombus - inicjalizowany dwoma przeciwległymi punktami i kątem przy nich
Rectangle - inicjalizowany dwoma przeciwległymi punktami i kątem (skierowanym) pomiędzy przekątną p1-p3 a bokiem p1-p2
Square - inicjalizowany dwoma przeciwległymi punktami.

Różnią się one tak, żeby uniknąć sytuacji, że mamy np. kwadrat, którego współrzędne wierzchołków wskazują na to, że wcale nie jest kwadratem, a te warunki wejściowe do konstruktorów są konieczne i wystarczające by powstała dana figura.

Poza tym zdefiniowałem sobie trochę pomocniczych klas-narzędzi geometrycznych (folder GeometryHelp) takie jak: Point, Vector, Segment (odcinek, wektor zaczepiony), Line (prosta), które zawierają pola definiujące je i podstawowe metody dokonujące operacji na nich. Poza tym jest jeszcze GeometricHelper, który zawiera bardziej skomplikowane statyczne metody, których argumentami jest kilka, często różnych klas, obiektów z folderu GeometryHelp.

Poza tym jedna klasa MathHelper, zawierająca jedną metodę sprawdzającą czy dwie liczby double są w przybliżeniu równe.

Poza tym folder FigureHelp - z enumem reprezentującym figury, oraz klasą FigureHelper, która:

  1. sprawdza, jaką figurą jest figura dana na wejściu, na podstawie rozmieszczenia wierzchołków (z przybliżeniem do 10 miejsca po przecinku) i zwraca odpowiednią "figurę" z enuma;

  2. zmienia figurze jeden wybrany punkt na inny.

    • nie podoba mi się rozwiązanie że ta metoda jest tu, a nie w figurze, owszem, ale konieczne było sprawdzenie czy wybrany przez usera punkt jest prawidłowy, tj. czy boki po zmianie się nie krzyżują. Do tego celu musiałem stworzyć Quadrilateral, aby móc sprawdzić, czy jest on prawidłowy. A jako że to Quadrilateral dziedziczy po Figure, Figure nie powinien mieć odniesienia na Quadrilateral, więc nie potrafi go stworzyć. A stworzyć instancji Figure, by zbadać czy boki się nie krzyżują nie można, bo jest to przecież klasa abstrakcyjna (i musi taka pozostać, z tych samych, niepodważalnych przyczyn)

Program najpierw tworzy 4 różne figury (dane testowe zhardkodowane w programie zobrazowane są tutaj: http://www.sendspace.pl/file/a30511f43c92a6f36d087b3), wypisuje wszystkie na ekranie. Potem tworzy tablicę na wskaźniki do figur i wrzuca do niej adresy tych 4 figur. Następnie daje userowi do ręki możliwość zmiany wierzchołków.

I tu wreszcie zaczyna się problem. O ile przed zmianą wszystko było ok - tzn. mimo, że wszystko było wskaźnikiem na figurę, to każda figura liczyła pole na swój sposób - wołana była prawidłowa funkcja wirtualna. Ale w momencie, gdy chcę zmienić mój kwadrat na prostokąt przystający do wbudowanego prostokąta -- oczywiście muszę to zrobić w dwóch krokach. Więc przykładowo przesuwam wierzchołek p4 na (-1, -3). Obecnie powinienem mieć czworokąt o polu i obwodzie (co już wyliczyłem) odpowiednio równych: 18 i ok. 28,6834. Jednak, mimo że po przesunięciu wierzchołka figura została poprawnie przez FigureHelper zinterpretowana jako czworokąt, i stworzony został potem czworokąt o prawidłowych wierzchołkach, to pole i obwod dalej jest liczone wg. sposobu kwadratu - wołane są metody Square::Area() oraz Square::Perimeter()!. Nie rozumiem czemu tak się dzieje. Przecież ta figura jest już czworokątem, nie kwadratem.

1

@punkt2
jest to naturalna "architektura" czy Ci sie podoba, czy nie.. dlaczego ta funkcja mialaby siedziec np. w prostokacie i zwracac kwadrat? czemu prostokat mialby w ogole wiedziec ze istnieje jakis "kwadrat"? Figura moglaby nie byc abstrakcyjna i moglaby to wszystko wiedziec, ale w zasadzie, skoro jest przeidziana jako baza do rozszerzania, to czemu mialoby sie w niej wypalac zamknieta wiedze o wszystkich mozliwych wystapieniach i typach figur? na takie rzeczy tworzy sie wlasnie osobne dedykowane klasy, czasem nawet zbudowane jako kupa luznych metod statycznych, ktore to klasy zajmuja sie wlasnie konwersja, detekcja cech, tlumaczeniem jednego w drugie itp. Dojda nowe typy? Bazy nie trzeba ruszac, dopisz nastepny konwerter..

@Problem
sprawdzilem rowniez, detecja i tworzenie quadrilateral przebiegaja poprawnie.
ale zdziwilo mnie kilka rzeczy.

  1. zapis figure = (Figure&)qd;
    przeciez QD dziedziczy po Figure. Po co tam (i wszedzie indziej..) owo rzutowanie? sprawdzilem, okazuje sie ze masz class Quadrilateral : Figure. Alez to jest dziedziczenie prywatne! Zrobiles tak specjalnie? jesli nie, usun tamte rzutowania i zmien na class Quadrilateral : public Figure
  2. Twoj wlasciwy problem tkwi w tej samej linii: figure = (Figure&)qd;
    sęk w tym, że C++ to nie Java ani C#. Operator= oznacza w C++ cos zupelnie innego niż tam. Tutaj, operator= jest ... metoda klasy stojacej po lewej stornie. W Javie czy C# operator = jest "globalny" i powoduje wymiane zawartosci zmiennej stojacej po lewej stronie.
    Co wiec sie u Ciebie w kodzie dzieje?
    Z lewej masz obiekt/zmienną Figura, faktycznie Kwadrat
    Z prawej masz obiekt/zmienna Figura(bo rzutujesz), faktycznie Quadrilateral
    odpala sie operator=
    .. ktorego NIE NAPISALES dla klasy Figura (lewej strony, ani po typie, ani faktycznej)

a co to oznacza? oznacza to, ze kompilator tak jak w przypadku brakujacych konstruktorow domyslnych oraz kopiujacych, dopisal sobie domyslny operator=
..ktory, domyslnie, kopiuje wszystkie POLA obiektu prawego do pol obiektu lewego.

uwaga. on kopiuje zawartosc pol, tak aby pola lewego byl rowne prawemu.
super rzecz dla struktur. ale co to oznacza dla obiektow i wirtualnosci? grzybnię!
to oznacza, ze z lewej masz nadal kwadrat! tylko o polach rownych prostokatowi stojacemu po prawej..
adres tego kwadratu SIE NIE ZMIENIL.
kwadrat nie stal sie nowym obiektem prostokata.

Mozesz to kojarzyc z efektem "object splicing", jest to bardzo bliskie, chociaz w Twoim przypadku malo widoczne, gdyz klasy-dzieci nie maja swoich nowych pol :)

czemu tak sie w ogole stalo?
ano, dlatego, ze:

  • nie nadpisales operatora=, wiec uzyty zostal domyslny, ale to pikus, wiekszy Twoj blad to:
  • Twoja funkcja pobiera KONKRETNY OBIEKT, pobiera Figura&. To znaczy ze moze "edytowac" ten obiekt, ale NIE MOZE go wymienic go na inny. Z zewnatrz funkcji masz wskazniki, prawda? otoz, pobierajac Figura& nie masz absolutnie zadnej mozliwosci wymiany obiektu czyli wymiany wpisu na nowy wskaznik do nowego obiektu. Widzisz.. obiekt raz stworzony jako Kwadrat pamieta ze jest kwadratem. Uzywajac sensownych metod, nie zmieniszjego typu juz nigdy. Aby zmienic go w obiekt-trojkat, musisz stworzyc NOWY obiekt, np. typu trojkat, a tamten stary obiekt np. usunac.

rozwiazanie:
olac na razie operator=, w tym akurat on i ta kCi nie pomoze..
Twoja funkcja powinna brzmiec:

void ChangePoint(Figure*& figure)   // <-- GWIAZDKA. operujesz na wskaznku-na, i dzieki & bedziesz wskaznik-na mogl wymienic
{
....
//	Quadrilateral qd;   nie
//	Square sq;    potrzebne
//	Rectangle rc;  wogole
//	Rhombus rh;
...
		delete figure;
		figure = new Quadrilateral(  // z ewentualnym rzutowaniem jesli upierasz sie na private inheritance
			figure->GetPoint(1),
			figure->GetPoint(2),
			figure->GetPoint(3),
			figure->GetPoint(4),
			"czworokat");
...

i pozniej jej wywolanie zmienia sie jedynie na

ChangePoint(figures[figureIndex - 1]); // uwaga, usunieta gwiazdka, teraz podawana jest ref-na-wskaznik-na-obiekt, abys mogl faktycznie wymienic wpis w tablicy na nowy obi

i.. tyle. doslownie, wymiana obiektu na nowy.

z przyzwyczajenia, zeby uniknac dziwactw podobnych, zwykle "z bomby" w nowopisanych klasach zabrania sie tworzenia copyctora oraz operatora=, dopisujac puste ich deklaracje z dopiskiem private.. a potem najwyzej dopisuje sie je jesli sa potrzebne faktycznie.

a jesli chcesz wiedziec czemu i jak obiekt pamieta kim jest, "mimo" operatora=, poczytaj o vtable, lub - zapytaj tutaj.

btw. moze od razu Ci do-podpowiem, ze skoro Twoje figury sa tworzone dynamicznie to:

	Square square = Square(Point(4, 2), Point(-2, -2), "kwadrat");
	Rectangle rectangle = Rectangle(Point(4, 2), Point(-2, -2),  -1.3734, "prostokat");
	Rhombus rhombus = Rhombus(Point(4, 4), Point(6, -6), 0.76101275422472977260717583, "romb");
	Quadrilateral quadrilateral = Quadrilateral(Point(-2, 1), Point(1, -3), Point(2, 2), Point(5, 5), "czworokat");

=>

	Square* square = new Square(Point(4, 2), Point(-2, -2), "kwadrat");
	Rectangle* rectangle = new Rectangle(Point(4, 2), Point(-2, -2),  -1.3734, "prostokat");
	Rhombus* rhombus = new Rhombus(Point(4, 4), Point(6, -6), 0.76101275422472977260717583, "romb");
	Quadrilateral* quadrilateral = new Quadrilateral(Point(-2, 1), Point(1, -3), Point(2, 2), Point(5, 5), "czworokat");

inaczej pierwszy lepszy delete w changefigure zachowa sie brzydko:)

0

Tak BTW, na wytłumaczenie wielu błędów - sporo przyzwyczajeń z C#...

@punkt2.

Niby tak. Ale rzecz w tym że punkty figury są cechą figury, a nie jakiegoś helpera. Więc to teoretycznie ona powinna mieć setter do punktów.

@Problem

Czemu robię to rzutowanie? temu:

error C2243: 'type cast' : conversion from 'Quadrilateral *' to 'const Figure &' exists, but is inaccessible

To się dzieje, gdy brak rzutowania. Czyli on nie potrafi w tym przypadku niejawnie wstawić Quadrilaterala do figury zapodanej jako parametr.
To, co bardziej mnie dziwi - Quadrilateral * - przecież wcale nie robiłem wskaźnika O.o

Aaa... dobra, teraz skojarzyłem - ten błąd co go zacytowałem jest dlatego, że jest prywatne dziedziczenie, tak? Czyli jak dam publiczne, będzie OK?

Nie kojarzę wgl "object splicing"...

super rzecz dla struktur

Gdzieś słyszałem, że, unlike w C# (typ referencyjny / typ wartościowy), w C++ struktury różnią się od klas tylko tym, że ich pola są domyślnie publiczne, a nie prywatne. Pewno się mylę :P Ale szukałem długo i pytałem długo i do tamtego momentu nikt nie potrafił mi wyjaśnić różnicy między klasą i strukturą...

Zdziw: Wydawało mi się że jak jest referencja, to jednak wymieni... Well. Nie pierwszy i nie ostatni raz się myliłem.

Co do argumentu ChangePoint, który proponujesz:

C++ Standard 8.3.2/4: napisał(a)

There shall be no references to references, no arrays of references, and no pointers to references.

qd, rc, rh i sq były potrzebne dla celów debuggerowawczych - bym mógł podejrzeć co tam właściwie się znajduje, zanim zostanie wstawione do Figure.

A tak bardzo chciałem uniknąć dynamicznych figur... :) W ostatnim projekcie (ten z macierzami i wektorami) miałem tyle dynamicznych rzeczy że już co krok to zamiast pisać nową funkcjonalność, tj. myśleć co pisać, cały czas zastanawiałem się jak to napisać, aby działało prawidłowo... Jak się cieszyłem kiedy to skończyłem. :D

Dobra, wypróbuję Twoje rozwiązanie, i zara dopiszę co z tego wyniknie... dzięki. :)

Edit:

delete figure;
figure = new Quadrilateral( // z ewentualnym rzutowaniem jesli upierasz sie na private inheritance
        figure->GetPoint(1),
        figure->GetPoint(2),
        figure->GetPoint(3),
        figure->GetPoint(4),
        "czworokat");

eee... nie jest tu pewien bubel? jeżeli zrobimy delete figure, to instrukcja figure->GetPoint(cokolwiek) walnie nullReferenceException. Czy mam rację?

1

aua, przepraszam, oczywiscie ze po delete figure nie ma co sie do tego odwolywac. walnąłem babola, bo najpierw napisalem wszystko, a potem na koncu dopisalem "delete".
poprawny fragment to oczywiscie:

Figure* tmp = figure;
figure = new Quadrilateral( // z ewentualnym rzutowaniem jesli upierasz sie na private inheritance
        figure->GetPoint(1),
        figure->GetPoint(2),
        figure->GetPoint(3),
        figure->GetPoint(4),
        "czworokat");
delete tmp;

--

Co do argumentu ChangePoint, który proponujesz:

"C++ Standard 8.3.2/4:":There shall be no references to references, no arrays of references, and no pointers to references.

Jeżeli STANDARD na to nie pozwala, to kompilator tez na to nie pozwoli, nie martw sie, patrz:
Figure* -- pointer to Figure
Figure& -- reference to Figure
Figure*& -- reference to pointer to Figure
Figure&* -- pointer to reference to Figure
czwarte jest NIEmożliwe, ja mówiłem o trzecim!

--
tak, musisz uwazac na rodzaj dziedziczenia. w C# jest TYLKO PUBLICZNE. w C++ jest domyslnie prywatne, opcjonalnie protected albo public, oraz jeszcze jedna rzecz, ktora bedzie Ci ciezko zrozumiec na poczatku - dziedziczenie 'virtual'. ale to czwarte zostaw sobie na pozniej.. na razie pobaw sie dziedziczeniem private/protected/public

--
tak, w C++ struktury i klasy nie roznia sie praktycznie niczym poza domyslna widocznoscia. nawet struktury moga miec funkcje wirtualne.

--
a "object splicing", w skrocie to jest taki dowcip, gdy (uwaga, pseudokod):

struct Ala { int a, b, c; }
struct Basia : Ala { int d,e,f; }

int main()
{
    Ala ala = {1,2,3}; 
    Basia basia = {10,20,30,40,50,60}; 
    Basia trzecia = {0,};   // uwaga! to jest inna BASIA

    ala = basia;  // splicing: Basia jest Alą, więc 'basia" jest "alą(10,20,30)", więc po tej linii, "ala" == "ala(10,20,30)" reszta Basi wyparowała
    ((Ala&)trzecia) = basia; // splicing: trzecia jest Basią, ale operator= widzi ja jako Ala& bo tak kazałes. efekt, po tej linii trzecia = Basia{10,20,30,0,0,0}
    trzecia = basia; // a tutaj wywola sie prawidlowy operator=, majacy po lewej stronie typ Basia, wiec efektem bedzie trzecia = Basia{10,20,30,40,50,60}

    // ale uwaga! nawet pomimo trzeciej linijki ktora wykonala sie sensownie, TRZECIA nadal JEST TRZECIĄ i nie jest tym samym obiektem co BASIA
    // operator= kopiuje ZAWARTOŚĆ zmiennych, a nie ICH TOŻSAMOŚĆ

tego typu efekty nazywaja sie "object splicing", czyli wycieciem z obiektu X jakiegos pod-obiektu Y, zazwyczaj bedacego "jego klasą bazową".
Twoj oryginalny przypadek też NIE zaliczał sie do tego problemu, ale był bardziej "haczykowaty", poniewaz u Ciebie nie nastepował splicing sensu stricte, czyli splicing "paczki danych" jakie obiekt w sobie ma, ale chodziło o to, ze operator= (przypisania) nie kopiuje pomiedzy obiektami czegos, co nazywa sie VTABLE, co istnieje TYLKO w przypadku klas/struktur ktore maja w sobie "coś wirtualnego". wlasnie owo VTABLE odpowiada za to, ze obiekt "pamieta kim jest/pamieta jakiego typu jest" i ze "umie wywolac wlaciwa metode mimo ze zmienna nie wie jaki jest jego prawdziwy typ"
object splicing jest "naturalny" dla jezyka C i struktur, w C++ stanowi duzo problemu, dopoki nie przyzwyczaisz sie, że:


Pokazuję Ci zarys problemu nazewniczego, ale to wyjasnienie ktore wlasnie napisalem **JEST BŁĘDNE**. Rozroznienie pomiedzy C++owa referencja, wskaznikiem, C# referencja oraz ref'em jest o wiele bardziej skomplikowane, i nie chcialbym na razie ie w to wdawac. Poprostu, traktuj te cztery rzeczy jako cztery totalnie różne terminy, które przypadkiem nazywaja sie "referencją".

Do tego co C# nazywa sie "referencją", w C++ najblizej jest właśnie wskaznikowi !!!
```cpp
    Basia* raz = &trzecia;
    Basia* dwa = &basia;

    raz = dwa;  // tutaj skopiowales TOZSAMOSC, poniewaz typ zmiennej jest Basia*. od tej pory RAZ i DWA pokazuja na ten sam obiekt
    raz = &trzecia; // zmiana na stara tozsamosc
    (*raz) = (*dwa); // uh.. a teraz skopiowanie zawartosci z prawej tozsamosci, do wewnatrz lewej tozszamosci..  to samo co zapisanie trzecia=basia tam wczesniej
```

Dlatego wlasnie, najblizszym tlumaczeniem Twojego zapisu:
void ChangeType(Figura& blah)    byloby w C#    void  ChangeType(Figura blah)
zaś mojemu zapisowi
void ChangeType(Figura*& blah)    odpowiada w C#    void ChangeType(ref Figura blah)
ale powtarzam, ref != &, ref != *&, ref != referencja, referencja != & :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1