programowanieProgramista C++

Co to jest dziedziczenie wirtualne w C++ i po co jest potrzebne?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Historia pytania

W C++ hierarchie klas mogą powodować problem diamentowy (diamond problem), gdy z jednej klasy bazowej dziedziczy dwóch potomków, a następnie tworzona jest kolejna klasa, która dziedziczy od tych dwóch. W takim przypadku obiekt klasy dziedziczącej będzie zawierał dwie niezależne kopie klasy bazowej. Aby rozwiązać ten problem, w C++ wprowadzono dziedziczenie wirtualne.

Problem

Jeśli użyjemy zwykłego dziedziczenia wielokrotnego:

class A { public: int x; }; class B : public A {}; class C : public A {}; class D : public B, public C {};

Obiekt D będzie zawierał 2 kopie A: jedną przez B, drugą — przez C. To powoduje niejednoznaczności w dostępie do A::x i nieuzasadnione wydatki pamięci.

Rozwiązanie

Dziedziczenie wirtualne eliminuje duplikację klasy bazowej. Jeden egzemplarz klasy bazowej A będzie używany przez wszystkich potomków:

class A { public: int x; }; class B : public virtual A {}; class C : public virtual A {}; class D : public B, public C {};

Teraz D zawiera tylko jedną kopię A, a niejednoznaczność z dostępem do A::x zostaje rozwiązana.

Kluczowe cechy:

  • Eliminacja problemu diamentowego (diamond problem)
  • Dziedziczenie wirtualne spowalnia dostęp do członków klasy bazowej
  • Konstruktory wirtualnych klas bazowych są wywoływane przez konstruktor najdalej położonego potomka

Pytania z pułapką.

Dlaczego nie można rozwiązać problemu diamentowego za pomocą jawnego wskazania ścieżki przez operator rozdzielenia zakresu?

Operator rozdzielenia zakresu rozwiązuje tylko niejednoznaczność dostępu do członków klasy bazowej, ale nie eliminuje zbędnych kopii samej klasy bazowej. Problem podwójnej inicjalizacji i podwójnego przechowywania danych pozostaje.

W jakiej kolejności są wywoływane konstruktory przy dziedziczeniu wirtualnym?

Konstruktory wirtualnych klas bazowych są wywoływane w pierwszej kolejności i tylko raz, przez konstruktor najdalej położonego w hierarchii dziedziczenia klasy.

Przykład:

class A { public: A() { std::cout << "A "; } }; class B : public virtual A { public: B() { std::cout << "B "; } }; class C : public virtual A { public: C() { std::cout << "C "; } }; class D : public B, public C { public: D() { std::cout << "D "; } }; D d; // Wydanie: A B C D

Czy konieczne jest deklarowanie dziedziczenia wirtualnego zarówno w deklaracji, jak i w definicji klasy?

Tak, dziedziczenie wirtualne musi być wskazane wszędzie, gdzie odbywa się dziedziczenie od odpowiedniej klasy bazowej. W przeciwnym razie błąd kompilacji jest nieunikniony, a dziedziczenie wielokrotne stanie się zwykłym.

Typowe błędy i antywzorce

  • Nie wskazano virtual przy dziedziczeniu — prowadzi to do pojawienia się dwóch egzemplarzy klasy bazowej
  • Próba wywołania konstruktora wirtualnej klasy bazowej we wszystkich klasach pośrednich — prowadzi do błędów kompilacji
  • Nadużywanie dziedziczenia wirtualnego komplikuje hierarchie i utrzymanie kodu

Przykład z życia

Negatywny przypadek

Złożona hierarchia, w której nie zastosowano dziedziczenia wirtualnego, przez co w obiekcie znajduje się dwie kopie ustawień:

Zalety:

  • Pozwala na łatwe kompilowanie i uruchamianie

Wady:

  • Danymi konfiguracji łatwo się zaplątać, co prowadzi do defektów „magicznych” rozmnażania danych bazowych

Pozytywny przypadek

Zastosowano dziedziczenie wirtualne z synchronizowanym konstruktorem najwyższego potomka:

Zalety:

  • Brak duplikacji danych bazowych, zachowanie jasne i proste

Wady:

  • Powstaje dodatkowa złożoność w analizie i debugowaniu dla programistów bez doświadczenia w pracy z dziedziczeniem wirtualnym