Chcę wiedzieć, czym jest „ wirtualna klasa bazowa ” i co ona oznacza.
Pokażę przykład:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
c++
virtual-inheritance
popopom
źródło
źródło
Odpowiedzi:
Wirtualne klasy podstawowe, używane w wirtualnym dziedziczeniu, są sposobem na zapobieganie pojawianiu się wielu „wystąpień” danej klasy w hierarchii dziedziczenia podczas korzystania z wielokrotnego dziedziczenia.
Rozważ następujący scenariusz:
Powyższa hierarchia klas skutkuje „przerażającym diamentem”, który wygląda następująco:
Instancja D będzie złożona z B, która obejmuje A, i C, która również obejmuje A. Zatem masz dwa „wystąpienia” (z braku lepszego wyrażenia) A.
Kiedy masz taki scenariusz, masz możliwość niejasności. Co się stanie, gdy to zrobisz:
Wirtualne dziedziczenie służy rozwiązaniu tego problemu. Kiedy określasz wirtualny podczas dziedziczenia klas, mówisz kompilatorowi, że chcesz tylko jednej instancji.
Oznacza to, że istnieje tylko jedna „instancja” A zawarta w hierarchii. W związku z tym
To jest mini podsumowanie. Aby uzyskać więcej informacji, przeczytaj to i to . Dobry przykład jest również dostępny tutaj .
źródło
virtual
, układ obiektu wygląda jak diament; a jeśli nie używamy,virtual
układ obiektu wygląda jak struktura drzewa, która zawiera dwaA
sO układzie pamięci
Na marginesie, problem z Przerażonym Diamentem polega na tym, że klasa podstawowa występuje wielokrotnie. Więc przy regularnym dziedziczeniu uważasz, że masz:
Ale w układzie pamięci masz:
To wyjaśnia, dlaczego podczas połączenia
D::foo()
masz problem z dwuznacznością. Ale prawdziwy problem pojawia się, gdy chcesz użyć zmiennej członkaA
. Załóżmy na przykład, że mamy:Gdy spróbujesz uzyskać dostęp
m_iValue
zD
, kompilator zaprotestuje, ponieważ w hierarchii zobaczy dwam_iValue
, a nie jeden. A jeśli zmodyfikujesz jeden, powiedzmyB::m_iValue
(to jestA::m_iValue
nadrzędnyB
),C::m_iValue
nie zostanie zmodyfikowany (to jestA::m_iValue
nadrzędnyC
).Tutaj przydaje się wirtualne dziedziczenie, ponieważ dzięki niemu wrócisz do prawdziwego układu diamentów, używając nie tylko jednej
foo()
metody, ale także jednej i tylko jednejm_iValue
.Co mogłoby pójść źle?
Wyobrażać sobie:
A
ma podstawową funkcję.B
dodaje do tego jakiś fajny zestaw danych (na przykład)C
dodaje do tego jakąś fajną funkcję, taką jak wzorzec obserwatora (na przykład włączonym_iValue
).D
dziedziczy poB
iC
, a zatem i odA
.W przypadku normalnego dziedziczenia zmiana
m_iValue
zD
jest niejednoznaczna i należy to rozwiązać. Nawet jeśli tak, są dwa wm_iValues
środkuD
, więc lepiej o tym pamiętaj i zaktualizuj oba jednocześnie.W przypadku wirtualnego dziedziczenia zmiana
m_iValue
zD
jest w porządku ... Ale ... Powiedzmy, że maszD
. Poprzez jegoC
interfejs dołączyłeś obserwatora. Za pomocą tegoB
interfejsu aktualizujesz fajną tablicę, której efektem ubocznym jest bezpośrednia zmianam_iValue
...Ponieważ zmiana
m_iValue
odbywa się bezpośrednio (bez użycia metody wirtualnego akcesorium), obserwator „nasłuchujący”C
nie zostanie wywołany, ponieważ kod implementujący nasłuchiwanie jest włączonyC
iB
nie wie o tym ...Wniosek
Jeśli masz diament w swojej hierarchii, oznacza to, że masz 95% prawdopodobieństwa, że zrobiłeś coś złego z tą hierarchią.
źródło
Wyjaśnienie wielokrotnego dziedziczenia za pomocą wirtualnych baz wymaga znajomości modelu obiektowego C ++. Wyjaśnienie tego tematu najlepiej jest zrobić w artykule, a nie w polu komentarza.
Najlepszym, czytelnym wyjaśnieniem, które rozwiązało wszystkie moje wątpliwości w tym temacie, był ten artykuł: http://www.phpcompiler.org/articles/virtualinheritance.html
Naprawdę nie będziesz musiał czytać niczego więcej na ten temat (chyba że jesteś pisarzem kompilatora) po przeczytaniu tego ...
źródło
Myślę, że mylisz dwie bardzo różne rzeczy. Dziedziczenie wirtualne nie jest tym samym, co klasa abstrakcyjna. Dziedziczenie wirtualne modyfikuje zachowanie wywołań funkcji; czasami rozwiązuje wywołania funkcji, które w innym przypadku byłyby niejednoznaczne, czasami odkłada obsługę wywołań funkcji na klasę inną niż ta, której można oczekiwać w dziedziczeniu innym niż wirtualny.
źródło
Chciałbym dodać do uprzejmych wyjaśnień Dz.U.
Dziedziczenie wirtualne nie przychodzi bez ceny. Podobnie jak w przypadku wszystkich rzeczy wirtualnych, masz hit wydajności. Istnieje sposób obejścia tego hitu wydajności, który może być mniej elegancki.
Zamiast łamać diament, czerpiąc wirtualnie, możesz dodać kolejną warstwę do diamentu, aby uzyskać coś takiego:
Żadna z klas nie dziedziczy wirtualnie, wszystkie dziedziczą publicznie. Klasy D21 i D22 będą wtedy ukrywać funkcję wirtualną f (), która jest niejednoznaczna dla DD, być może przez uznanie tej funkcji za prywatną. Każdy z nich zdefiniowałby odpowiednio funkcję opakowania, odpowiednio f1 () i f2 (), każde wywołujące klasę lokalną (prywatną) f (), rozwiązując w ten sposób konflikty. Klasa DD wywołuje f1 (), jeśli chce D11 :: f () i f2 (), jeśli chce D12 :: f (). Jeśli zdefiniujesz zawijanie w linii, prawdopodobnie uzyskasz około zera narzutu.
Oczywiście, jeśli możesz zmienić D11 i D12, możesz wykonać tę samą sztuczkę w tych klasach, ale często tak nie jest.
źródło
Oprócz tego, co już powiedziano na temat wielokrotnego i wirtualnego dziedziczenia, istnieje bardzo interesujący artykuł na temat Dr Dobb's Journal: Wielokrotne dziedziczenie uważane za przydatne
źródło
Jesteś trochę zagmatwany. Nie wiem, czy mieszacie jakieś koncepcje.
Nie masz wirtualnej klasy bazowej w swoim OP. Masz tylko klasę podstawową.
Zrobiłeś wirtualne dziedzictwo. Jest to zwykle używane w przypadku wielokrotnego dziedziczenia, dzięki czemu wiele klas pochodnych korzysta z elementów klasy podstawowej bez ich odtwarzania.
Klasa podstawowa z czystą funkcją wirtualną nie jest tworzona. wymaga to składni, którą zna Paul. Jest zwykle używany, aby klasy pochodne musiały zdefiniować te funkcje.
Nie chcę więcej o tym wyjaśniać, ponieważ nie rozumiem, o co pytasz.
źródło
Oznacza to, że wywołanie funkcji wirtualnej zostanie przekierowane do „właściwej” klasy.
C ++ FAQ Lite FTW.
Krótko mówiąc, często stosuje się go w scenariuszach wielokrotnego dziedziczenia, w których powstaje hierarchia „diamentowa”. Dziedziczenie wirtualne przełamie dwuznaczność powstałą w dolnej klasie, gdy wywołasz funkcję w tej klasie, a funkcja musi zostać rozstrzygnięta na klasę D1 lub D2 powyżej tej dolnej klasy. Diagram i szczegóły znajdują się w części FAQ .
Jest również stosowany w delegacji siostrzanej , co jest potężną funkcją (choć nie dla osób o słabym sercu). Zobacz to FAQ.
Zobacz także punkt 40 w Efektywnym trzecim wydaniu C ++ (43 w drugim wydaniu).
źródło
Przykład użycia dziedziczonego diamentu
Ten przykład pokazuje, jak używać wirtualnej klasy bazowej w typowym scenariuszu: w celu rozwiązania dziedziczenia diamentów.
źródło
assert(A::aDefault == 0);
z głównej funkcji daje mi błąd kompilacji:aDefault is not a member of A
użycie gcc 5.4.0. Co to ma robić?Klasy wirtualne to nie to samo, co wirtualne dziedziczenie. Wirtualne klasy, których nie można utworzyć, wirtualne dziedziczenie to coś zupełnie innego.
Wikipedia opisuje to lepiej niż potrafię. http://en.wikipedia.org/wiki/Virtual_inheritance
źródło
Regularne dziedziczenie
W przypadku typowego 3-poziomowego nie-diamentowego dziedziczenia niebędącego wirtualnym dziedziczeniem podczas tworzenia nowego najbardziej pochodnego obiektu wywoływane jest nowe, a rozmiar wymagany dla obiektu jest określany przez typ kompilatora i przekazywany do nowego.
nowy ma podpis:
I wykonuje połączenie
malloc
, zwracając wskaźnik pustkiJest on następnie przekazywany do konstruktora najbardziej pochodnego obiektu, który natychmiast wywoła środkowy konstruktor, a następnie środkowy konstruktor natychmiast wywoła konstruktor podstawowy. Baza następnie przechowuje wskaźnik do wirtualnej tabeli na początku obiektu, a następnie za nim atrybutów. Następnie wraca do środkowego konstruktora, który zapisze swój wirtualny wskaźnik tabeli w tej samej lokalizacji, a następnie jego atrybuty po atrybutach, które byłyby przechowywane przez konstruktora podstawowego. Powraca do najbardziej pochodnego konstruktora, który przechowuje wskaźnik do swojej wirtualnej tabeli w tym samym miejscu, a następnie jego atrybuty po atrybutach, które byłyby przechowywane przez środkowy konstruktor.
Ponieważ wskaźnik wirtualnej tabeli jest nadpisywany, wskaźnik wirtualnej tabeli zawsze jest jedną z najbardziej pochodnych klas. Wirtualność rozprzestrzenia się w kierunku klasy najbardziej pochodnej, więc jeśli funkcja jest wirtualna w klasie średniej, będzie wirtualna w klasie najbardziej pochodnej, ale nie w klasie bazowej. Jeśli odrzucisz polimorficznie instancję klasy najbardziej pochodnej do wskaźnika do klasy podstawowej, kompilator nie rozwiąże tego w wywołaniu pośrednim do tabeli wirtualnej, a zamiast tego wywoła funkcję bezpośrednio
A::function()
. Jeśli funkcja jest wirtualna dla typu, do którego ją rzuciłeś, to rozwiąże wywołanie do wirtualnej tabeli, która zawsze będzie klasą najbardziej pochodną. Jeśli nie jest wirtualny dla tego typu, po prostu wywołaType::function()
i przekaże do niego wskaźnik obiektu, rzutując na Typ.W rzeczywistości, gdy mówię wskaźnik do wirtualnej tabeli, w rzeczywistości zawsze jest to przesunięcie o 16 względem wirtualnej tabeli.
virtual
nie jest ponownie wymagany w klasach pochodnych, jeśli jest wirtualny w klasie mniej pochodnej, ponieważ się propaguje. Ale można go użyć do pokazania, że funkcja jest rzeczywiście funkcją wirtualną, bez konieczności sprawdzania klas, które dziedziczy definicje typów.override
jest kolejną ochroną kompilatora, która mówi, że ta funkcja coś przesłania, a jeśli nie, to zgłasza błąd kompilatora.= 0
oznacza, że jest to funkcja abstrakcyjnafinal
zapobiega ponownemu wdrożeniu funkcji wirtualnej w klasie bardziej pochodnej i upewni się, że wirtualna tabela najbardziej pochodnej klasy zawiera ostateczną funkcję tej klasy.= default
wyjaśnia w dokumentacji, że kompilator użyje domyślnej implementacji= delete
podać błąd kompilatora, jeśli nastąpi próba wywołania tegoDziedziczenie wirtualne
Rozważać
Bez dziedziczenia klasy basowej otrzymasz obiekt, który wygląda następująco:
Zamiast tego:
Tj. Będą 2 obiekty podstawowe.
W wirtualnym diamentowej spadków powyżej sytuacji, gdy nowy nazywa, wywołuje konstruktor większości pochodzący iw tym konstruktora, wywołuje wszystkie 3 konstruktorów pochodzące przechodzących przesunięć w swoim wirtualnym stół, zamiast dzwonić po prostu dzwoni
DerivedClass1::DerivedClass1()
iDerivedClass2::DerivedClass2()
czym te oba wywołującyBase::Base()
Wszystko zostało skompilowane w trybie debugowania -O0, więc będzie zbędny zestaw
Wywołuje
Base::Base()
wskaźnik za pomocą przesunięcia obiektu 32. Baza przechowuje wskaźnik do wirtualnej tabeli pod adresem, który otrzymuje, a członkowie po nim.DerivedDerivedClass::DerivedDerivedClass()
następnie wywołujeDerivedClass1::DerivedClass1()
za pomocą wskaźnika do obiektu offset 0, a także przekazuje adresVTT for DerivedDerivedClass+8
DerivedDerivedClass::DerivedDerivedClass()
następnie przekazuje adres obiektu + 16 i adres dla VTTDerivedDerivedClass+24
doDerivedClass2::DerivedClass2()
których montaż jest identycznaDerivedClass1::DerivedClass1()
z wyjątkiem liniimov DWORD PTR [rax+8], 3
, która ma oczywiście 4 zamiast 3 nad = 4
.Następnie zastępuje wszystkie 3 wirtualne wskaźniki tabeli w obiekcie wskaźnikami przesunięć w tabeli
DerivedDerivedClass
vtable do reprezentacji dla tej klasy.d->VirtualFunction();
:d->DerivedCommonFunction();
:d->DerivedCommonFunction2();
:d->DerivedDerivedCommonFunction();
:((DerivedClass2*)d)->DerivedCommonFunction2();
:((Base*)d)->VirtualFunction();
:źródło