Jak C ++ obsługuje wielokrotne dziedziczenie ze wspólnym wspólnym przodkiem?

13

Nie jestem facetem w C ++, ale muszę o tym myśleć. Dlaczego wielokrotne dziedziczenie jest możliwe w C ++, ale nie w C #? (Wiem o problemie z diamentem , ale nie o to tu pytam). Jak C ++ rozwiązuje niejednoznaczność identycznych podpisów metod odziedziczonych z wielu klas podstawowych? I dlaczego ten sam projekt nie jest włączony do C #?

Sandeep
źródło
3
Dla kompletności, jaki jest „problem z diamentem”?
jcolebrand
1
@jcolebrand pl.wikipedia.org/wiki/Multiple_inheritance patrz problem z
diamentem
1
Jasne, przejrzałem go, ale skąd mam wiedzieć, że to właśnie oznacza Sandeep? Jeśli masz zamiar odwoływać się do niejasnej nazwy, gdy „Wielokrotne dziedziczenie ze wspólnym wspólnym przodkiem” jest bardziej bezpośrednie ...
jcolebrand 30.01.2013
1
@jcolebrand Zredagowałem go, aby odzwierciedlić to, co otrzymałem z pytania. Zakładam, że ma na myśli problem diamentów, do którego odwołuje się wikipedia z kontekstu. Następnym razem upuść przyjazną komentarz i użyć nowego narzędzia błyszczący zaproponował edycji :)
Earlz
1
@Ellz, który działa tylko wtedy, gdy rozumiem odniesienie. W przeciwnym razie dokonam złej edycji, a to po prostu złe juju.
jcolebrand

Odpowiedzi:

24

Dlaczego wielokrotne dziedziczenie jest możliwe w C ++, ale nie w C #?

Myślę (bez twardego odniesienia), że w Javie chcieli ograniczyć ekspresję języka, aby uczynić go łatwiejszym do nauczenia się, a ponieważ kod wykorzystujący wielokrotne dziedziczenie jest często zbyt skomplikowany dla własnego dobra. A ponieważ pełne wielokrotne dziedziczenie jest o wiele bardziej skomplikowane do wdrożenia, dlatego też znacznie uprościło maszynę wirtualną (wielokrotne dziedziczenie szczególnie źle współdziała z modułem wyrzucania elementów bezużytecznych, ponieważ wymaga utrzymywania wskaźników na środku obiektu (na początku bazy) )

I projektując C #, myślę, że spojrzeli na Javę, zauważyli, że pełne wielokrotne dziedziczenie rzeczywiście nie było wiele pominięte i postanowili również uprościć sprawę.

Jak C ++ rozwiązuje niejednoznaczność identycznych podpisów metod odziedziczonych z wielu klas podstawowych?

Tak nie jest . Istnieje składnia umożliwiająca jawne wywołanie metody klasy bazowej z konkretnej bazy, ale nie ma sposobu, aby zastąpić tylko jedną z metod wirtualnych, a jeśli nie zastąpisz metody w podklasie, nie będzie możliwe jej wywołanie bez określenia bazy klasa.

I dlaczego ten sam projekt nie jest włączony do C #?

Nie ma nic do włączenia.


Ponieważ Giorgio wspominał o metodach rozszerzenia interfejsu w komentarzach, wyjaśnię, czym są miksy i jak są one implementowane w różnych językach.

Interfejsy w Javie i C # są ograniczone tylko do metod deklarowania. Ale metody muszą być zaimplementowane w każdej klasie, która dziedziczy interfejs. Istnieje jednak duża klasa interfejsów, w których przydatne byłoby zapewnienie domyślnych implementacji niektórych metod w odniesieniu do innych. Typowy przykład jest porównywalny (w pseudo-języku):

mixin IComparable {
    public bool operator<(IComparable r) = 0;
    public bool operator>(IComparable r) { return r < this; }
    public bool operator<=(IComparable r) { return !(r < this); }
    public bool operator>=(IComparable r) { return !(r > this); }
    public bool operator==(IComparable r) { return !(r < this) && !(r > this); }
    public bool operator!=(IComparable r) { return r < this || r > this; }
};

Różnica w stosunku do pełnej klasy polega na tym, że nie może zawierać żadnych elementów danych. Istnieje kilka opcji realizacji tego. Oczywiście wielokrotne dziedziczenie jest jednym. Ale wdrożenie wielokrotnego dziedziczenia jest raczej skomplikowane. Ale tutaj tak naprawdę nie jest to potrzebne. Zamiast tego wiele języków implementuje to, dzieląc mixin w interfejsie, który jest implementowany przez klasę i repozytorium implementacji metod, które są albo wstrzykiwane do samej klasy, albo generowana jest pośrednia klasa bazowa i tam umieszczane. Jest to zaimplementowane w Ruby i D , będzie zaimplementowane w Javie 8 i może być zaimplementowane ręcznie w C ++ przy użyciu ciekawie powtarzającego się wzorca szablonu . Powyższe w formie CRTP wygląda następująco:

template <typename Derived>
class IComparable {
    const Derived &_d() const { return static_cast<const Derived &>(*this); }
public:
    bool operator>(const IComparable &r) const { r._d() < _d(); }
    bool operator<=(const IComparable &r) const { !(r._d() < _d(); }
    ...
};

i jest używany jak:

class Concrete : public IComparable<Concrete> { ... };

Nie wymaga to deklarowania niczego wirtualnego, tak jak zrobiłaby to zwykła klasa podstawowa, więc jeśli interfejs jest używany w szablonach, pozostają użyteczne opcje optymalizacji otwarte. Zauważ, że w C ++ prawdopodobnie nadal byłby dziedziczony jako drugi element nadrzędny, ale w językach, które nie pozwalają na wielokrotne dziedziczenie, jest wstawiany do łańcucha pojedynczego dziedziczenia, więc bardziej przypomina

template <typename Derived, typename Base>
class IComparable : public Base { ... };
class Concrete : public IComparable<Concrete, Base> { ... };

Implementacja kompilatora może, ale nie musi, unikać wirtualnej wysyłki.

W C # wybrano inną implementację. W języku C # implementacje są metodami statycznymi całkowicie oddzielnej klasy, a składnia wywołania metody jest odpowiednio interpretowana przez kompilator, jeśli metoda o podanej nazwie nie istnieje, ale zdefiniowano „metodę rozszerzenia”. Ma to tę zaletę, że metody rozszerzeń można dodawać do już skompilowanej klasy, a wadą jest to, że takich metod nie można zastąpić, np. W celu zapewnienia zoptymalizowanej wersji.

Jan Hudec
źródło
Można wspomnieć, że do Java 8 wprowadzono wielokrotne dziedziczenie za pomocą metod rozszerzenia interfejsu.
Giorgio
@Giorgio: Nie, wielokrotne dziedziczenie na pewno nie zostanie wprowadzone w Javie. Będą to mieszanki, co jest zupełnie inną rzeczą, chociaż obejmuje wiele pozostałych powodów używania wielokrotnego dziedziczenia i większość powodów używania ciekawie powtarzającego się wzorca szablonu (CRTP) i działa głównie jak CRTP, a nie jak wielokrotne dziedziczenie.
Jan Hudec
Myślę, że wielokrotne dziedziczenie nie wymaga wskaźników na środku obiektu. Gdyby tak było, wymagałoby to również dziedziczenia wielu interfejsów.
svick
@svick: Nie, nie ma. Ale alternatywa jest znacznie mniej wydajna, ponieważ wymaga wirtualnej wysyłki dla dostępu członków.
Jan Hudec
+1 za znacznie bardziej kompletną odpowiedź niż moja.
Nathan C. Tresch
2

Odpowiedź brzmi: nie działa poprawnie w C ++ w przypadku kolizji przestrzeni nazw. Zobacz to . Aby uniknąć kolizji przestrzeni nazw, musisz wykonywać wszelkiego rodzaju zawirowania za pomocą wskaźników. Pracowałem w MS w zespole Visual Studio i przynajmniej częściowo powodem, dla którego opracowali delegację, było całkowite uniknięcie kolizji przestrzeni nazw. Uprzednio powiedziałem, że uważali również interfejsy za część rozwiązania wielokrotnego dziedziczenia, ale się myliłem. Interfejsy są naprawdę niesamowite i można je uruchomić w C ++, FWIW.

Delegowanie dotyczy konkretnie kolizji przestrzeni nazw: możesz delegować do 5 klas, a wszystkie 5 z nich wyeksportuje swoje metody do twojego zakresu jako członkowie pierwszej klasy. Na zewnątrz patrząc na to JEST wielokrotne dziedzictwo.

Nathan C. Tresch
źródło
1
Uważam, że jest bardzo mało prawdopodobne, aby był to główny powód, dla którego nie uwzględniono MI w języku C #. Co więcej, ten post nie odpowiada na pytanie, dlaczego MI działa w C ++, gdy nie masz „kolizji przestrzeni nazw”.
Doc Brown
@DocBrown Pracowałem w MS w zespole Visual Studio i zapewniam cię, że przynajmniej częściowo dlatego opracowali delegację i interfejsy, aby całkowicie uniknąć kolizji przestrzeni nazw. Co do jakości pytania, meh. Użyj swojego zdania, inni uważają, że jest to przydatne.
Nathan C. Tresch
1
Nie mam zamiaru głosować za odpowiedzią, ponieważ uważam, że jest poprawna. Ale twoja odpowiedź udaje, że MI jest całkowicie bezużyteczny w C ++, i dlatego nie wprowadzili go w języku C #. Chociaż nie bardzo lubię MI w C ++, myślę, że nie jest to całkowicie niewykonalne.
Doc Brown
1
To jeden z powodów i nie chciałem sugerować, że to nigdy nie działa. Kiedy coś jest niedeterministyczne w przetwarzaniu, zwykle mówię, że jest „zepsute” lub, że „nie działa”, kiedy zamierzam powiedzieć, że „jest jak voodoo, może, ale nie musi, zapalić świece i modlić się”. : D
Nathan C. Tresch
1
czy „kolizje przestrzeni nazw” nie są deterministyczne?
Doc Brown