Warsztat » Forum

[C++] Overriding - kiedy następuje?

Nov 14, 2009 | Kos |
5 wypowiedzi na 1 stronach:
1
Kos
Nov 14, 2009

Overriding - kiedy następuje?

Mamy klasę A oraz klasę B dziedziczącą z A. Klasa A ma metodę wirtualną XYZ. Klasa B ma metodę o nazwie XYZ.

Jak rozpoznać, który scenariusz zajdzie podczas kompilacji? Mogą się stać 3 rzeczy:

a) B::XYZ będzie overridem dla A::XYZ
b) Poleci błąd kompilatora
c) Kod skompiluje się, ale B::XYZ nie będzie overridem dla A::XYZ

Mój punkt widzenia:

1) Sprawdzamy ilość i typy argumentów. B::XYZ Musi być *dokładnie* taka sama jeśli chodzi o typy - z moich eksperymentów wynika że żadne superklasy i subklasy nie mają tu znaczenia, każda różnica powoduje od razu scenariusz C - czyli cichy bug, którego szuka się debuggerem tydzień :). Wyjątek: B::XYZ może zaproponować wartości domyślne dla argumentów, czyli f(int a) można overridować za pomocą f(int a=5).

2) Jeśli pkt 1 przeszedł, sprawdzamy typ zwracany. Jeśli są takie same LUB typ zwracany B::XYZ jest covariant z typem zwracanym przez A::XYZ, to otrzymujemy scenariusz A. W przeciwnym wypadku otrzymujemy scenariusz B z błędem w stylu "invalid covariant return type for `virtual Typ B::XYZ()'".

Pytanie 1: Co dokładnie znaczy covariant w tym kontekście? Jeśli A zwraca wskaźnik/referencję na obiekt jakiejś klasy, to na pewno możemy zaostrzyć wymagania oferując w B wskaźnik/referencję na obiekt subklasy. Wskaźnik na void* możemy doprecyzować wskaźnikiem na cokolwiek. Czy są jakieś inne opcje?

Pytanie 2: Czy specyfikatory dostępu (są tu trzy: przy metodzie A::XYZ, przy metodzie B::XYZ i przy dziedziczeniu B:A) mają w tym procesie JAKIEKOLWIEK znaczenie? Wydaje mi się to dziwne, bo nie dostrzegłem nic takiego - nawet, jeśli metodę private overridujemy metodą public przy dziedziczeniu prywatnym! Bardzo mnie to zdziwiło. A może coś pokręciłem?

Wszelkie dokładne doprecyzowania mile widziane.
Xion
Nov 14, 2009

Odp: Overriding - kiedy następuje?

Co do pytania 1, to hasło 'covariant return types' w dokumentacji/Google jest tym, czego powinieneś szukać. Względnie możesz przeczytać moje wypociny na ten temat

Co do pytania 2, to nie powinno być to dziwne. Gdyby specyfikatory dostępu miały znaczenie, to wywołanie B::XYZ odnosiłoby się do innej funkcji w zależności od tego, czy jest z wnętrza B, czy spoza niego. Biorąc pod uwagę typową implementację metod wirtualnych (czyli vtable + vptr) nie widzę łatwego sposobu, jak dałoby się takie rozróżnienie poczynić. Pewnie m.in. dlatego w języku overriding działa bez względu na to, czy pierwotna wersja metody jest dostępna czy nie.
Krzysiek K.
Nov 14, 2009

Odp: Overriding - kiedy następuje?

Cytat:
Wszelkie dokładne doprecyzowania mile widziane.

Metody metody nie-const nie overriduja metod const (i chyba na odwrót też, ale nie jestem pewien).
Kos
Nov 18, 2009

Odp: Overriding - kiedy następuje?

@2x up - thanks za link!

@up - dzięki, mamy jedną rzecz którą pominąłem. :) Sprawdziłem - musi być tak samo. Inaczej mamy sytuację C, niezależne metody.

Przypomniało mi to o kolejnym szczególe: deklaracja throws() dla metody.
(bardzo proszę, nie dyskutujmy w tym temacie czy używanie tej deklaracji ma sens - możemy na to założyć drugi)

Żeby nie było za łatwo, throw() podlega własnemu zestawowi reguł: w metodzie overridującej specyfikator throw musi być ściślejszy, niż w bazowej. W przeciwnym wypadku - na szczęście - dostajemy B, czyli błąd kompilacji.
Teraz: Co to znaczy "ściślejszy specyfikator throw"?* Domyślny specyfikator (czyli jego brak) pozwala na rzucenie czegokolwiek, więc każdy jest od niego ściślejszy. Z kolei pusty (throw()) nie pozwala na rzucanie niczego, więc można go nadpisać jedynie takim samym.
* Ogólniej: Jeśli B chce nadpisać A, to każdy element z deklaracji throw dla B musi zawierać się lub być "throw-rzutowalny" na jakiś element z deklaracji throw dla A.

Napisałem "throw-rzutowalny", bo ze zwykłą rzutowalnością nie jest to tożsame. Zauważyłem na przykład, że o ile dla najprostszego przypadku wskaźników na obiekty klas dziedziczących ładnie działa, ale jeśli w nadrzędnej damy throws(void*), a w podrzędnej throws(typ*), to już nie przejdzie - WTF? O co biega? Przypomnę, że w kowariancji typu zwracanego nie było takich problemów.

A może bug kompilatora? Wydaje mi się to bardzo nielogiczne, więc podam, że mam g++ (GCC) 3.4.5 (mingw-vista special r3), a jakiś samarytanin z 4.3 lub 4.4 mógłby zechcieć spróbować skompilować to:
Kod: cpp]
struct A {
    virtual void foo() throw(void*) {}
};
struct B : private A {
    void foo() throw(int*) {}
};


Błąd mój:
Kod: C:\Users\Kos\Desktop>g++ asd2.cpp -c -o asd2.o
asd2.cpp:5: error: looser throw specifier for `virtual void B::foo() throw (int*)'
asd2.cpp:2: error:   overriding `virtual void A::foo() throw (void*)'

Jeśli zamiast void* i int* damy 2 klasy, nadrzędną i podrzędną, to wszystko skompiluje się gładko.

Odpowiednik z kowariancją się kompiluje ładnie:
Kod: cpp]struct A {
    virtual void* foo() {}
};
struct B : private A {
    int* foo() {}
};


Jest jakiś logiczny powód lub wyjaśnienie dla takiej niespójności?

[hr]

Jeszcze jedno pytanie: Czy gdzieś tutaj (przy throw() lub kowariancji) można znaleźć jakieś tajemnicze kruczki z dziedziczeniem wielokrotnym, które nam sprawę jeszcze bardziej skomplikują?
owyn
Nov 16, 2009

Odp: Overriding - kiedy następuje?

Cytat:

Co do pytania 2, to nie powinno być to dziwne. Gdyby specyfikatory dostępu miały znaczenie, to wywołanie B::XYZ odnosiłoby się do innej funkcji w zależności od tego, czy jest z wnętrza B, czy spoza niego. Biorąc pod uwagę typową implementację metod wirtualnych (czyli vtable + vptr) nie widzę łatwego sposobu, jak dałoby się takie rozróżnienie poczynić. Pewnie m.in. dlatego w języku overriding działa bez względu na to, czy pierwotna wersja metody jest dostępna czy nie.

Moim zdaniem takie coś jest dość mylące i może spowodować przypadkowe naruszenie zasad enkapsulacji. Weźmy np. taki kod:
Kod: 

class A {
public:
   virtual void test() {
      cout << "A" << endl;
   }
};

class B : public A {
private:
   void test() {
      cout << "B" << endl;
   }
};

Funkcja B::test jest prywatna i jako taka powinna być możliwa do wywołania wyłącznie z innej funkcji klasy B. W tym przykładzie w zasadzie jest dostępna z każdego miejsca kodu, w dodatku żeby to zauważyć, trzeba przejrzeć kod innej klasy. Takie coś powinno przynajmniej generować ostrzeżenie kompilatora. W Javie jest to w ogóle niedopuszczalne (błąd kompilacji), w C#, z tego co pamiętam, też.
Strony:
1