25.8 -- Wirtualne klasy bazowe

Ostatni rozdział w lekcji 24.9 -- Dziedziczenie wielokrotne, przestaliśmy rozmawiać o „problemie diamentu”. W tej sekcji wznowimy tę dyskusję.

Uwaga: Ta sekcja jest tematem zaawansowanym i w razie potrzeby można ją pominąć lub przejrzeć.

Problem diamentu

Oto nasz przykład z poprzedniej lekcji (z niektórymi konstruktorami) ilustrujący problem diamentu:

#include <iostream>

class PoweredDevice
{
public:
    PoweredDevice(int power)
    {
		std::cout << "PoweredDevice: " << power << '\n';
    }
};

class Scanner: public PoweredDevice
{
public:
    Scanner(int scanner, int power)
        : PoweredDevice{ power }
    {
		std::cout << "Scanner: " << scanner << '\n';
    }
};

class Printer: public PoweredDevice
{
public:
    Printer(int printer, int power)
        : PoweredDevice{ power }
    {
		std::cout << "Printer: " << printer << '\n';
    }
};

class Copier: public Scanner, public Printer
{
public:
    Copier(int scanner, int printer, int power)
        : Scanner{ scanner, power }, Printer{ printer, power }
    {
    }
};

Chociaż możesz spodziewać się diagramu dziedziczenia wyglądającego tak:

Gdybyś miał to zrobić utwórz obiekt klasy Copier, domyślnie otrzymasz dwie kopie klasy PoweredDevice — jedną z drukarki i jedną ze skanera. Ma to następującą strukturę:

Możemy stworzyć krótki przykład, który pokaże to w działaniu:

int main()
{
    Copier copier{ 1, 2, 3 };

    return 0;
}

Daje to wynik:

PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2

Jak widzisz, PoweredDevice zostało skonstruowane dwukrotnie.

Choć jest to często pożądane, innym razem możesz chcieć, aby tylko jedna kopia PoweredDevice była współdzielona przez skaner i drukarkę.

Wirtualne klasy bazowe

Aby udostępnić klasę bazową, po prostu wstaw słowo kluczowe „virtual” w polu lista dziedziczenia klasy pochodnej. Tworzy to tak zwaną wirtualną klasę bazową, co oznacza, że ​​istnieje tylko jeden obiekt bazowy. Obiekt bazowy jest współdzielony pomiędzy wszystkimi obiektami w drzewie dziedziczenia i jest tworzony tylko raz. Oto przykład (bez konstruktorów dla uproszczenia) pokazujący, jak używać słowa kluczowego virtual do tworzenia współdzielonej klasy bazowej:

class PoweredDevice
{
};

class Scanner: virtual public PoweredDevice
{
};

class Printer: virtual public PoweredDevice
{
};

class Copier: public Scanner, public Printer
{
};

Teraz, gdy utworzysz obiekt klasy Copier, otrzymasz tylko jedną kopię PoweredDevice na kopiarkę, która będzie współdzielona zarówno przez skaner, jak i drukarkę.

Prowadzi to jednak do jeszcze jednego problemu: jeśli skaner i drukarka współdzielą klasę bazową PoweredDevice, kto jest odpowiedzialny za jej utworzenie? Jak się okazuje, odpowiedzią jest kopiarka. Konstruktor Copier jest odpowiedzialny za utworzenie PoweredDevice. W rezultacie jest to jeden raz, kiedy Copier może wywołać bezpośrednio konstruktor inny niż bezpośredni rodzic:

#include <iostream>

class PoweredDevice
{
public:
    PoweredDevice(int power)
    {
		std::cout << "PoweredDevice: " << power << '\n';
    }
};

class Scanner: virtual public PoweredDevice // note: PoweredDevice is now a virtual base class
{
public:
    Scanner(int scanner, int power)
        : PoweredDevice{ power } // this line is required to create Scanner objects, but ignored in this case
    {
		std::cout << "Scanner: " << scanner << '\n';
    }
};

class Printer: virtual public PoweredDevice // note: PoweredDevice is now a virtual base class
{
public:
    Printer(int printer, int power)
        : PoweredDevice{ power } // this line is required to create Printer objects, but ignored in this case
    {
		std::cout << "Printer: " << printer << '\n';
    }
};

class Copier: public Scanner, public Printer
{
public:
    Copier(int scanner, int printer, int power)
        : PoweredDevice{ power }, // PoweredDevice is constructed here
        Scanner{ scanner, power }, Printer{ printer, power }
    {
    }
};

Tym razem nasz poprzedni przykład:

int main()
{
    Copier copier{ 1, 2, 3 };

    return 0;
}

da wynik:

PoweredDevice: 3
Scanner: 1
Printer: 2

Jak widać, PoweredDevice jest konstruowany tylko raz.

Jest kilka szczegółów, o których nie wspomnielibyśmy, gdybyśmy nie wspomnieli.

Po pierwsze, konstruktor najbardziej pochodnej klasy, bazy wirtualnej klasy są zawsze tworzone przed niewirtualnymi klasami bazowymi, co gwarantuje, że wszystkie bazy zostaną utworzone przed klasami pochodnymi.

Po drugie, zauważ, że konstruktory Scanner i Printer nadal mają wywołania konstruktora PoweredDevice. Podczas tworzenia instancji Copier te wywołania konstruktora są po prostu ignorowane, ponieważ Copier jest odpowiedzialny za utworzenie PoweredDevice, a nie skanera czy drukarki. Gdybyśmy jednak utworzyli instancję Scanner lub Printer, użyte zostałyby te wywołania konstruktora i miałyby zastosowanie normalne zasady dziedziczenia.

Po trzecie, jeśli klasa dziedziczy jedną lub więcej klas, które mają wirtualnych rodziców, najbardziej wyprowadzana klasa jest odpowiedzialna za konstruowanie wirtualnej klasy bazowej. W tym przypadku Copier dziedziczy drukarkę i skaner, które mają wirtualną klasę bazową PoweredDevice. Copier, najbardziej pochodna klasa, jest odpowiedzialna za utworzenie PoweredDevice. Należy zauważyć, że jest to prawdą nawet w przypadku pojedynczego dziedziczenia: jeśli Copier pojedynczo odziedziczył po Printer, a Printer został wirtualnie odziedziczony po PoweredDevice, Copier jest nadal odpowiedzialny za utworzenie PoweredDevice.

Po czwarte, wszystkie klasy dziedziczące wirtualną klasę bazową będą miały wirtualną tabelę, nawet jeśli normalnie by jej nie miały inaczej, a zatem instancje klasy będą większe o wskaźnik.

Ponieważ Skaner i Drukarka wywodzą się praktycznie z PoweredDevice, Kopiarka będzie tylko jednym podobiektem PoweredDevice. Zarówno skaner, jak i drukarka muszą wiedzieć, jak znaleźć ten pojedynczy podobiekt PoweredDevice, aby móc uzyskać dostęp do jego elementów składowych (ponieważ w końcu pochodzą z niego). Zwykle odbywa się to za pomocą magii wirtualnej tabeli (która zasadniczo przechowuje przesunięcie każdej podklasy do podobiektu PoweredDevice).

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
133 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze